with class="link-action" and a data-url), will cause
-// the browser to redirect to the target page.
-type redirectObject struct {
- Redirect string `json:"redirect"`
+ ctx.JSON(http.StatusOK, resp)
}
// Rerun will rerun jobs in the given run
@@ -493,7 +383,7 @@ func Rerun(ctx *context_module.Context) {
run.PreviousDuration = run.Duration()
run.Started = 0
run.Stopped = 0
- if err := actions_service.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
+ if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
@@ -505,7 +395,6 @@ func Rerun(ctx *context_module.Context) {
}
if jobIndexStr == "" { // rerun all jobs
- var redirectURL string
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
@@ -513,31 +402,13 @@ func Rerun(ctx *context_module.Context) {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
- if redirectURL == "" {
- // ActionRunJob's `Attempt` field won't be updated to reflect the rerun until the job is picked by a
- // runner. But we need to redirect the user somewhere; if they stay on the current attempt then the
- // rerun's logs won't appear. So, we redirect to the upcoming new attempt and then we'll handle the
- // weirdness in the UI if the attempt doesn't exist yet.
- j.Attempt++ // note: this is intentionally not persisted
- redirectURL, err = j.HTMLURL(ctx)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- }
- }
-
- if redirectURL != "" {
- ctx.JSON(http.StatusOK, &redirectObject{Redirect: redirectURL})
- } else {
- ctx.Error(http.StatusInternalServerError, "unable to determine redirectURL for job rerun")
}
+ ctx.JSON(http.StatusOK, struct{}{})
return
}
rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
- var redirectURL string
for _, j := range rerunJobs {
// jobs other than the specified one should be set to "blocked" status
shouldBlock := j.JobID != job.JobID
@@ -545,22 +416,9 @@ func Rerun(ctx *context_module.Context) {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
- if j.JobID == job.JobID {
- // see earlier comment about redirectURL, applicable here as well
- j.Attempt++ // note: this is intentionally not persisted
- redirectURL, err = j.HTMLURL(ctx)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- }
}
- if redirectURL != "" {
- ctx.JSON(http.StatusOK, &redirectObject{Redirect: redirectURL})
- } else {
- ctx.Error(http.StatusInternalServerError, "unable to determine redirectURL for job rerun")
- }
+ ctx.JSON(http.StatusOK, struct{}{})
}
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
@@ -578,7 +436,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
job.Stopped = 0
if err := db.WithTx(ctx, func(ctx context.Context) error {
- _, err := actions_service.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
+ _, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
return err
}); err != nil {
return err
@@ -654,16 +512,16 @@ func Cancel(ctx *context_module.Context) {
if job.TaskID == 0 {
job.Status = actions_model.StatusCancelled
job.Stopped = timeutil.TimeStampNow()
- n, err := actions_service.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
+ 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")
+ return fmt.Errorf("job has changed, try again")
}
continue
}
- if err := actions_service.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
+ if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
return err
}
}
@@ -691,13 +549,13 @@ func Approve(ctx *context_module.Context) {
if err := db.WithTx(ctx, func(ctx context.Context) error {
run.NeedApproval = false
run.ApprovedBy = doer.ID
- if err := actions_service.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
+ 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
- _, err := actions_service.UpdateRunJob(ctx, job, nil, "status")
+ _, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
@@ -761,27 +619,19 @@ type ArtifactsViewItem struct {
func ArtifactsView(ctx *context_module.Context) {
runIndex := ctx.ParamsInt64("run")
- artifactsResponse := getArtifactsViewResponse(ctx, runIndex)
- if ctx.Written() {
- return
- }
- ctx.JSON(http.StatusOK, artifactsResponse)
-}
-
-func getArtifactsViewResponse(ctx *context_module.Context, runIndex int64) *ArtifactsViewResponse {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, err.Error())
- return nil
+ return
}
ctx.Error(http.StatusInternalServerError, err.Error())
- return nil
+ return
}
artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
- return nil
+ return
}
artifactsResponse := ArtifactsViewResponse{
Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)),
@@ -797,7 +647,7 @@ func getArtifactsViewResponse(ctx *context_module.Context, runIndex int64) *Arti
Status: status,
})
}
- return &artifactsResponse
+ ctx.JSON(http.StatusOK, artifactsResponse)
}
func ArtifactsDeleteView(ctx *context_module.Context) {
@@ -818,82 +668,30 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, struct{}{})
}
-func getRunByID(ctx *context_module.Context, runID int64) *actions_model.ActionRun {
- if runID == 0 {
- log.Debug("Requested runID is zero.")
- ctx.Error(http.StatusNotFound, "zero is not a valid run ID")
- return nil
- }
-
- run, has, err := actions_model.GetRunByIDWithHas(ctx, runID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return nil
- }
- if !has {
- log.Debug("Requested runID[%d] not found.", runID)
- ctx.Error(http.StatusNotFound, fmt.Sprintf("no such run %d", runID))
- return nil
- }
- if run.RepoID != ctx.Repo.Repository.ID {
- log.Debug("Requested runID[%d] does not belong to repo[%-v].", runID, ctx.Repo.Repository)
- ctx.Error(http.StatusNotFound, "no such run")
- return nil
- }
- return run
-}
-
-func artifactsFind(ctx *context_module.Context, opts actions_model.FindArtifactsOptions) []*actions_model.ActionArtifact {
- artifacts, err := db.Find[actions_model.ActionArtifact](ctx, opts)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return nil
- }
- if len(artifacts) == 0 {
- return nil
- }
- return artifacts
-}
-
-func artifactsFindByNameOrID(ctx *context_module.Context, runID int64, nameOrID string) []*actions_model.ActionArtifact {
- artifacts := artifactsFind(ctx, actions_model.FindArtifactsOptions{
- RunID: runID,
- ArtifactName: nameOrID,
- })
- if ctx.Written() {
- return nil
- }
- // if lookup by name found nothing, maybe it is an ID
- if len(artifacts) == 0 {
- id, err := strconv.ParseInt(nameOrID, 10, 64)
- if err != nil || id == 0 {
- ctx.Error(http.StatusNotFound, fmt.Sprintf("runID %d: artifact name not found: %v", runID, nameOrID))
- return nil
- }
- artifacts = artifactsFind(ctx, actions_model.FindArtifactsOptions{
- RunID: runID,
- ID: id,
- })
- if ctx.Written() {
- return nil
- }
- if len(artifacts) == 0 {
- ctx.Error(http.StatusNotFound, fmt.Sprintf("runID %d: artifact ID not found: %v", runID, nameOrID))
- return nil
- }
- }
- return artifacts
-}
-
func ArtifactsDownloadView(ctx *context_module.Context) {
- run := getRunByID(ctx, ctx.ParamsInt64("run"))
- if ctx.Written() {
+ runIndex := ctx.ParamsInt64("run")
+ artifactName := ctx.Params("artifact_name")
+
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, err.Error())
+ return
+ }
+ ctx.Error(http.StatusInternalServerError, err.Error())
return
}
- artifactNameOrID := ctx.Params("artifact_name_or_id")
- artifacts := artifactsFindByNameOrID(ctx, run.ID, artifactNameOrID)
- if ctx.Written() {
+ artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
+ RunID: run.ID,
+ ArtifactName: artifactName,
+ })
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
+ }
+ if len(artifacts) == 0 {
+ ctx.Error(http.StatusNotFound, "artifact not found")
return
}
@@ -922,14 +720,12 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
- common.ServeContentByReadSeeker(ctx.Base, artifacts[0].ArtifactName+".zip", util.ToPointer(art.UpdatedUnix.AsTime()), f)
+ common.ServeContentByReadSeeker(ctx.Base, artifactName, util.ToPointer(art.UpdatedUnix.AsTime()), f)
return
}
// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
// Those need to be zipped for download
- artifactName := artifacts[0].ArtifactName
-
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
writer := zip.NewWriter(ctx.Resp)
defer writer.Close()
diff --git a/routers/web/repo/actions/view_test.go b/routers/web/repo/actions/view_test.go
deleted file mode 100644
index e3f72e9e37..0000000000
--- a/routers/web/repo/actions/view_test.go
+++ /dev/null
@@ -1,512 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package actions
-
-import (
- "fmt"
- "html/template"
- "net/http"
- "testing"
-
- actions_model "forgejo.org/models/actions"
- repo_model "forgejo.org/models/repo"
- unittest "forgejo.org/models/unittest"
- "forgejo.org/modules/json"
- "forgejo.org/modules/web"
- "forgejo.org/services/contexttest"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func Test_getRunByID(t *testing.T) {
- unittest.PrepareTestEnv(t)
-
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 5, ID: 4})
-
- for _, testCase := range []struct {
- name string
- runID int64
- err string
- }{
- {
- name: "Found",
- runID: 792,
- },
- {
- name: "NotFound",
- runID: 24344,
- err: "no such run",
- },
- {
- name: "ZeroNotFound",
- runID: 0,
- err: "zero is not a valid run ID",
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- ctx, resp := contexttest.MockContext(t, fmt.Sprintf("user5/repo4/actions/runs/%v/artifacts/some-name", testCase.runID))
- ctx.Repo.Repository = repo
- run := getRunByID(ctx, testCase.runID)
- if testCase.err == "" {
- assert.NotNil(t, run)
- assert.False(t, ctx.Written(), resp.Body.String())
- } else {
- assert.Nil(t, run)
- assert.True(t, ctx.Written())
- assert.Contains(t, resp.Body.String(), testCase.err)
- }
- })
- }
-}
-
-func Test_artifactsFind(t *testing.T) {
- unittest.PrepareTestEnv(t)
-
- for _, testCase := range []struct {
- name string
- artifactName string
- count int
- }{
- {
- name: "Found",
- artifactName: "artifact-v4-download",
- count: 1,
- },
- {
- name: "NotFound",
- artifactName: "notexist",
- count: 0,
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- runID := int64(792)
- ctx, _ := contexttest.MockContext(t, fmt.Sprintf("user5/repo4/actions/runs/%v/artifacts/%v", runID, testCase.artifactName))
- artifacts := artifactsFind(ctx, actions_model.FindArtifactsOptions{
- RunID: runID,
- ArtifactName: testCase.artifactName,
- })
- assert.False(t, ctx.Written())
- assert.Len(t, artifacts, testCase.count)
- })
- }
-}
-
-func Test_artifactsFindByNameOrID(t *testing.T) {
- unittest.PrepareTestEnv(t)
-
- for _, testCase := range []struct {
- name string
- nameOrID string
- err string
- }{
- {
- name: "NameFound",
- nameOrID: "artifact-v4-download",
- },
- {
- name: "NameNotFound",
- nameOrID: "notexist",
- err: "artifact name not found",
- },
- {
- name: "IDFound",
- nameOrID: "22",
- },
- {
- name: "IDNotFound",
- nameOrID: "666",
- err: "artifact ID not found",
- },
- {
- name: "IDZeroNotFound",
- nameOrID: "0",
- err: "artifact name not found",
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- runID := int64(792)
- ctx, resp := contexttest.MockContext(t, fmt.Sprintf("user5/repo4/actions/runs/%v/artifacts/%v", runID, testCase.nameOrID))
- artifacts := artifactsFindByNameOrID(ctx, runID, testCase.nameOrID)
- if testCase.err == "" {
- assert.NotEmpty(t, artifacts)
- assert.False(t, ctx.Written(), resp.Body.String())
- } else {
- assert.Empty(t, artifacts)
- assert.True(t, ctx.Written())
- assert.Contains(t, resp.Body.String(), testCase.err)
- }
- })
- }
-}
-
-func baseExpectedViewResponse() *ViewResponse {
- return &ViewResponse{
- State: ViewState{
- Run: ViewRunInfo{
- Link: "/user5/repo4/actions/runs/187",
- Title: "update actions",
- TitleHTML: template.HTML("update actions"),
- Status: "success",
- CanCancel: false,
- CanApprove: false,
- CanRerun: false,
- CanDeleteArtifact: false,
- Done: true,
- Jobs: []*ViewJob{
- {
- ID: 192,
- Name: "job_2",
- Status: "success",
- CanRerun: false,
- Duration: "1m38s",
- },
- },
- Commit: ViewCommit{
- LocaleCommit: "actions.runs.commit",
- LocalePushedBy: "actions.runs.pushed_by",
- LocaleWorkflow: "actions.runs.workflow",
- ShortSha: "c2d72f5484",
- Link: "/user5/repo4/commit/c2d72f548424103f01ee1dc02889c1e2bff816b0",
- Pusher: ViewUser{
- DisplayName: "user1",
- Link: "/user1",
- },
- Branch: ViewBranch{
- Name: "master",
- Link: "/user5/repo4/src/branch/master",
- IsDeleted: false,
- },
- },
- },
- CurrentJob: ViewCurrentJob{
- Title: "job_2",
- Detail: "actions.status.success",
- Steps: []*ViewJobStep{
- {
- Summary: "Set up job",
- Status: "running",
- },
- {
- Summary: "Complete job",
- Status: "waiting",
- },
- },
- AllAttempts: []*TaskAttempt{
- {
- Number: 3,
- Started: template.HTML("2023-05-09 12:48:48 +00:00 "),
- Status: "running",
- },
- {
- Number: 2,
- Started: template.HTML("2023-05-09 12:48:48 +00:00 "),
- Status: "success",
- },
- {
- Number: 1,
- Started: template.HTML("2023-05-09 12:48:48 +00:00 "),
- Status: "success",
- },
- },
- },
- },
- Logs: ViewLogs{
- StepsLog: []*ViewStepLog{},
- },
- }
-}
-
-func TestActionsViewViewPost(t *testing.T) {
- unittest.PrepareTestEnv(t)
-
- tests := []struct {
- name string
- runIndex int64
- jobIndex int64
- attemptNumber int64
- expected *ViewResponse
- expectedTweaks func(*ViewResponse)
- }{
- {
- name: "base case",
- runIndex: 187,
- jobIndex: 0,
- attemptNumber: 1,
- expected: baseExpectedViewResponse(),
- expectedTweaks: func(resp *ViewResponse) {
- resp.State.CurrentJob.Steps[0].Status = "success"
- resp.State.CurrentJob.Steps[1].Status = "success"
- },
- },
- {
- name: "run with waiting jobs",
- runIndex: 189,
- jobIndex: 0,
- attemptNumber: 1,
- expected: baseExpectedViewResponse(),
- expectedTweaks: func(resp *ViewResponse) {
- // Variations from runIndex 187 -> runIndex 189 that are not the subject of this test...
- resp.State.Run.Link = "/user5/repo4/actions/runs/189"
- resp.State.Run.Title = "job output"
- resp.State.Run.TitleHTML = "job output"
- resp.State.Run.Jobs = []*ViewJob{
- {
- ID: 194,
- Name: "job1 (1)",
- Status: "success",
- },
- {
- ID: 195,
- Name: "job1 (2)",
- Status: "success",
- },
- {
- ID: 196,
- Name: "job2",
- Status: "waiting",
- },
- }
- resp.State.CurrentJob.Title = "job1 (1)"
- resp.State.CurrentJob.Steps = []*ViewJobStep{
- {
- Summary: "Set up job",
- Status: "success",
- },
- {
- Summary: "Complete job",
- Status: "success",
- },
- }
- resp.State.CurrentJob.AllAttempts = []*TaskAttempt{
- {
- Number: 1,
- Started: template.HTML("2023-05-09 12:48:48 +00:00 "),
- Status: "success",
- },
- }
-
- // Under test in this case: verify that Done is set to false; in the fixture data, job.ID=195 is status
- // Success, but job.ID=196 is status Waiting, and so we expect to signal Done=false to indicate to the
- // UI to continue refreshing the page.
- resp.State.Run.Done = false
- },
- },
- {
- name: "attempt 3",
- runIndex: 187,
- jobIndex: 0,
- attemptNumber: 3,
- expected: baseExpectedViewResponse(),
- expectedTweaks: func(resp *ViewResponse) {
- resp.State.CurrentJob.Steps[0].Status = "running"
- resp.State.CurrentJob.Steps[1].Status = "waiting"
- },
- },
- {
- // This ActionRunJob has TaskID: null, which allows us to access out-of-range attempts without errors and
- // with just some stub data for the UI to start waiting around on.
- name: "attempt out-of-bounds on non-picked task",
- runIndex: 190,
- jobIndex: 0,
- attemptNumber: 100,
- expected: baseExpectedViewResponse(),
- expectedTweaks: func(resp *ViewResponse) {
- // Variations from runIndex 187 -> runIndex 190 that are not the subject of this test...
- resp.State.Run.Link = "/user5/repo4/actions/runs/190"
- resp.State.Run.Title = "job output"
- resp.State.Run.TitleHTML = "job output"
- resp.State.Run.Done = false
- resp.State.Run.Jobs = []*ViewJob{
- {
- ID: 396,
- Name: "job_2",
- Status: "waiting",
- },
- }
- resp.State.Run.Commit.Branch = ViewBranch{
- Name: "test",
- Link: "/user5/repo4/src/branch/test",
- IsDeleted: true,
- }
-
- // Expected blank data in the response because this job isn't picked by a runner yet. Keep details here
- // in-sync with the RepoActionView 'view non-picked action run job' test.
- resp.State.CurrentJob.Detail = "actions.status.waiting"
- resp.State.CurrentJob.Steps = []*ViewJobStep{}
- resp.State.CurrentJob.AllAttempts = nil
- },
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ctx, resp := contexttest.MockContext(t, "user2/repo1/actions/runs/0")
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadRepo(t, ctx, 4)
- ctx.SetParams(":run", fmt.Sprintf("%d", tt.runIndex))
- ctx.SetParams(":job", fmt.Sprintf("%d", tt.jobIndex))
- ctx.SetParams(":attempt", fmt.Sprintf("%d", tt.attemptNumber))
- web.SetForm(ctx, &ViewRequest{})
-
- ViewPost(ctx)
- require.Equal(t, http.StatusOK, resp.Result().StatusCode, "failure in ViewPost(): %q", resp.Body.String())
-
- var actual ViewResponse
- err := json.Unmarshal(resp.Body.Bytes(), &actual)
- require.NoError(t, err)
-
- // `Duration` field is dynamic based upon current time, so eliminate it from comparison -- but check that it
- // has the right format at least.
- zeroDurations := func(vr *ViewResponse) {
- for _, job := range vr.State.Run.Jobs {
- assert.Regexp(t, `^(\d+[hms]){1,3}$`, job.Duration)
- job.Duration = ""
- }
- for _, step := range vr.State.CurrentJob.Steps {
- step.Duration = ""
- }
- }
- zeroDurations(&actual)
- zeroDurations(tt.expected)
- tt.expectedTweaks(tt.expected)
-
- assert.Equal(t, *tt.expected, actual)
- })
- }
-}
-
-func TestActionsViewRedirectToLatestAttempt(t *testing.T) {
- unittest.PrepareTestEnv(t)
-
- tests := []struct {
- name string
- runIndex int64
- jobIndex int64
- expectedCode int
- expectedURL string
- userID int64
- repoID int64
- }{
- {
- name: "no job index",
- runIndex: 187,
- jobIndex: -1,
- expectedURL: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/0/attempt/1",
- },
- {
- name: "job w/ 1 attempt",
- runIndex: 187,
- jobIndex: 0,
- expectedURL: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/0/attempt/1",
- },
- {
- name: "job w/ multiple attempts",
- runIndex: 187,
- jobIndex: 2,
- expectedURL: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/2/attempt/2",
- },
- {
- name: "run out-of-range",
- runIndex: 5000,
- jobIndex: -1,
- expectedCode: http.StatusNotFound,
- },
- // Odd behavior with an out-of-bound jobIndex -- defaults to the first job. This is existing behavior
- // documented in the getRunJobs internal helper which... seems not perfect for the redirect... but it's high
- // risk to change and it's an OK user outcome to be redirected to something valid in the requested run.
- {
- name: "job out-of-range",
- runIndex: 187,
- jobIndex: 500,
- expectedURL: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/0/attempt/1",
- },
- // This ActionRunJob has Attempt: 0 and TaskID: null, which indicates its first run is pending pickup by a
- // runner. Should redirect to the attempt/1 since that's what it will be when it is running.
- {
- name: "redirect to non-picked task",
- userID: 2,
- repoID: 4,
- runIndex: 190,
- jobIndex: 0,
- expectedURL: "https://try.gitea.io/user5/repo4/actions/runs/190/jobs/0/attempt/1",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ctx, resp := contexttest.MockContext(t, "user2/repo1/actions/runs/0")
- if tt.userID == 0 {
- contexttest.LoadUser(t, ctx, 2)
- } else {
- contexttest.LoadUser(t, ctx, tt.userID)
- }
- if tt.repoID == 0 {
- contexttest.LoadRepo(t, ctx, 1)
- } else {
- contexttest.LoadRepo(t, ctx, tt.repoID)
- }
- ctx.SetParams(":run", fmt.Sprintf("%d", tt.runIndex))
- if tt.jobIndex != -1 {
- ctx.SetParams(":job", fmt.Sprintf("%d", tt.jobIndex))
- }
-
- RedirectToLatestAttempt(ctx)
- if tt.expectedCode == 0 {
- assert.Equal(t, http.StatusTemporaryRedirect, resp.Code)
- url, err := resp.Result().Location()
- require.NoError(t, err)
- assert.Equal(t, tt.expectedURL, url.String())
- } else {
- assert.Equal(t, tt.expectedCode, resp.Code)
- }
- })
- }
-}
-
-func TestActionsRerun(t *testing.T) {
- tests := []struct {
- name string
- runIndex int64
- jobIndex int64
- expectedCode int
- expectedURL string
- }{
- {
- name: "rerun all",
- runIndex: 187,
- jobIndex: -1,
- expectedURL: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/0/attempt/2",
- },
- {
- name: "rerun job",
- runIndex: 187,
- jobIndex: 2,
- expectedURL: "https://try.gitea.io/user2/repo1/actions/runs/187/jobs/2/attempt/3",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ctx, resp := contexttest.MockContext(t, "user2/repo1/actions/runs/187/rerun")
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadRepo(t, ctx, 1)
- ctx.SetParams(":run", fmt.Sprintf("%d", tt.runIndex))
- if tt.jobIndex != -1 {
- ctx.SetParams(":job", fmt.Sprintf("%d", tt.jobIndex))
- }
-
- Rerun(ctx)
- require.Equal(t, http.StatusOK, resp.Result().StatusCode, "failure in Rerun(): %q", resp.Body.String())
-
- var actual redirectObject
- err := json.Unmarshal(resp.Body.Bytes(), &actual)
- require.NoError(t, err)
-
- // Note: this test isn't doing any functional testing of the Rerun handler's actual ability to set up a job
- // rerun. This test was added when the redirect to the correct `attempt` was added and only covers that
- // addition at this time.
- assert.Equal(t, redirectObject{Redirect: tt.expectedURL}, actual)
- })
- }
-}
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index 5b2eaef889..e46c08fef8 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -106,7 +106,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
}
if repository == nil { // If not linked
- if !ctx.IsSigned || attach.UploaderID != ctx.Doer.ID { // We block if not the uploader
+ if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
ctx.Error(http.StatusNotFound)
return
}
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index f4cc2a2cea..ccdd59f2dd 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -82,19 +82,19 @@ func RefBlame(ctx *context.Context) {
return
}
+ ctx.Data["NumLinesSet"] = true
+ ctx.Data["NumLines"], err = blob.GetBlobLineCount()
+ if err != nil {
+ ctx.ServerError("GetBlobLineCount", err)
+ return
+ }
+
result, err := performBlame(ctx, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormBool("bypass-blame-ignore"))
if err != nil {
ctx.ServerError("performBlame", err)
return
}
- ctx.Data["NumLinesSet"] = true
- numLines := 0
- for _, p := range result.Parts {
- numLines += len(p.Lines)
- }
- ctx.Data["NumLines"] = numLines
-
ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs
ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 0fe52bfb48..af8a838fc9 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -70,6 +70,11 @@ func Branches(ctx *context.Context) {
ctx.ServerError("LoadBranches", err)
return
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ for key := range commitStatuses {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
+ }
+ }
commitStatus := make(map[string]*git_model.CommitStatus)
for commitID, cs := range commitStatuses {
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
index 44c07e617e..04009b4afa 100644
--- a/routers/web/repo/code_frequency.go
+++ b/routers/web/repo/code_frequency.go
@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
ctx.Status(http.StatusAccepted)
return
}
- ctx.ServerError("GetContributorStats", err)
+ ctx.ServerError("GetCodeFrequencyData", err)
} else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 408a2844de..3cd80a6777 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -16,6 +16,7 @@ import (
"forgejo.org/models/db"
git_model "forgejo.org/models/git"
repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"forgejo.org/modules/base"
"forgejo.org/modules/charset"
@@ -83,19 +84,8 @@ func Commits(ctx *context.Context) {
ctx.ServerError("CommitsByRange", err)
return
}
- ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository)
+ ctx.Data["Commits"] = processGitCommits(ctx, commits)
- commitIDs := make([]string, 0, len(commits))
- for _, c := range commits {
- commitIDs = append(commitIDs, c.ID.String())
- }
- commitTagsMap, err := repo_model.FindTagsByCommitIDs(ctx, ctx.Repo.Repository.ID, commitIDs...)
- if err != nil {
- log.Error("FindTagsByCommitIDs: %v", err)
- ctx.Flash.Error(ctx.Tr("repo.commit.load_tags_failed"))
- } else {
- ctx.Data["CommitTagsMap"] = commitTagsMap
- }
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["CommitCount"] = commitsCount
@@ -212,7 +202,7 @@ func SearchCommits(ctx *context.Context) {
return
}
ctx.Data["CommitCount"] = len(commits)
- ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository)
+ ctx.Data["Commits"] = processGitCommits(ctx, commits)
ctx.Data["Keyword"] = query
if all {
@@ -277,7 +267,7 @@ func FileHistory(ctx *context.Context) {
}
}
- ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository)
+ ctx.Data["Commits"] = processGitCommits(ctx, commits)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
@@ -339,7 +329,7 @@ func Diff(ctx *context.Context) {
maxLines, maxFiles = -1, -1
}
- diff, err := gitdiff.GetDiffFull(ctx, gitRepo, &gitdiff.DiffOptions{
+ diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{
AfterCommitID: commitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
@@ -385,6 +375,9 @@ func Diff(ctx *context.Context) {
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
+ if !ctx.Repo.CanRead(unit_model.TypeActions) {
+ git_model.CommitStatusesHideActionsURL(ctx, statuses)
+ }
ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses)
ctx.Data["CommitStatuses"] = statuses
@@ -463,6 +456,20 @@ func RawDiff(ctx *context.Context) {
}
}
+func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) []*git_model.SignCommitWithStatuses {
+ commits := git_model.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository)
+ if !ctx.Repo.CanRead(unit_model.TypeActions) {
+ for _, commit := range commits {
+ if commit.Status == nil {
+ continue
+ }
+ commit.Status.HideActionsURL(ctx)
+ git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
+ }
+ }
+ return commits
+}
+
func SetCommitNotes(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CommitNotesForm)
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index feedeef945..db65e889e0 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -312,16 +312,22 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
if !baseIsCommit && !baseIsBranch && !baseIsTag {
- if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
+ // 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))
}
+ return nil
} else {
ctx.NotFound("IsRefExist", nil)
+ return nil
}
- return nil
}
ctx.Data["BaseIsCommit"] = baseIsCommit
ctx.Data["BaseIsBranch"] = baseIsBranch
@@ -508,8 +514,15 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
headIsBranch := ci.HeadGitRepo.IsBranchExist(ci.HeadBranch)
headIsTag := ci.HeadGitRepo.IsTagExist(ci.HeadBranch)
if !headIsCommit && !headIsBranch && !headIsTag {
- ctx.NotFound("IsRefExist", nil)
- return nil
+ // 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("IsRefExist", nil)
+ return nil
+ }
}
ctx.Data["HeadIsCommit"] = headIsCommit
ctx.Data["HeadIsBranch"] = headIsBranch
@@ -520,6 +533,17 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
}
+ if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
+ if log.IsTrace() {
+ log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
+ ctx.Doer,
+ baseRepo,
+ permBase)
+ }
+ ctx.NotFound("ParseCompareInfo", nil)
+ return nil
+ }
+
baseBranchRef := ci.BaseBranch
if baseIsBranch {
baseBranchRef = git.BranchPrefix + ci.BaseBranch
@@ -573,7 +597,7 @@ func PrepareCompareDiff(
config := unit.PullRequestsConfig()
if !config.AutodetectManualMerge {
- allowEmptyPr := ci.BaseBranch != ci.HeadBranch || ctx.Repo.Repository.Name != ci.HeadRepo.Name
+ allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
ctx.Data["AllowEmptyPr"] = allowEmptyPr
return !allowEmptyPr
@@ -597,7 +621,7 @@ func PrepareCompareDiff(
fileOnly := ctx.FormBool("file-only")
- diff, err := gitdiff.GetDiffFull(ctx, ci.HeadGitRepo,
+ diff, err := gitdiff.GetDiff(ctx, ci.HeadGitRepo,
&gitdiff.DiffOptions{
BeforeCommitID: beforeCommitID,
AfterCommitID: headCommitID,
@@ -630,15 +654,15 @@ func PrepareCompareDiff(
return false
}
- commits := git_model.ParseCommitsWithStatus(ctx, ci.CompareInfo.Commits, ctx.Repo.Repository)
+ commits := processGitCommits(ctx, ci.CompareInfo.Commits)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
if len(commits) == 1 {
c := commits[0]
- title = strings.TrimSpace(c.Summary())
+ title = strings.TrimSpace(c.UserCommit.Summary())
- body := strings.Split(strings.TrimSpace(c.Message()), "\n")
+ body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
if len(body) > 1 {
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
@@ -909,33 +933,28 @@ func ExcerptBlob(ctx *context.Context) {
ctx.Error(http.StatusInternalServerError, "getExcerptLines")
return
}
-
- // After the "up" or "down" expansion, check if there's any remaining content in the diff and add a line that will
- // be rendered into a new expander at either the top, or bottom.
- lineSection := &gitdiff.DiffLine{
- Type: gitdiff.DiffLineSection,
- SectionInfo: &gitdiff.DiffLineSectionInfo{
- Path: filePath,
- LastLeftIdx: lastLeft,
- LastRightIdx: lastRight,
- LeftIdx: idxLeft,
- RightIdx: idxRight,
- LeftHunkSize: leftHunkSize,
- RightHunkSize: rightHunkSize,
- },
- }
- if lineSection.GetExpandDirection() != gitdiff.DiffLineExpandNone {
+ 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.Content = lineText
-
- switch direction {
- case "up":
+ 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,
+ },
+ }
+ if direction == "up" {
section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
- case "down":
+ } else if direction == "down" {
section.Lines = append(section.Lines, lineSection)
}
}
@@ -947,7 +966,7 @@ func ExcerptBlob(ctx *context.Context) {
}
func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chunkSize int) ([]*gitdiff.DiffLine, error) {
- blob, err := commit.GetBlobByPath(filePath)
+ blob, err := commit.Tree.GetBlobByPath(filePath)
if err != nil {
return nil, err
}
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index 9fb4d78fe3..fc82ece4cb 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -92,7 +92,7 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
return nil, nil
}
- if entry.IsDir() || entry.IsSubmodule() {
+ if entry.IsDir() || entry.IsSubModule() {
ctx.NotFound("getBlobForEntry", nil)
return nil, nil
}
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 3e3cb0016d..5114cc9c05 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -189,7 +189,7 @@ func editFile(ctx *context.Context, isNewFile bool) {
buf = buf[:n]
// Only some file types are editable online as text.
- if !typesniffer.DetectContentType(buf, blob.Name()).IsRepresentableAsText() {
+ if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
return
}
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index b5d40abdab..5b893cf258 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -37,7 +37,7 @@ func TestCleanUploadName(t *testing.T) {
"..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
}
for k, v := range kases {
- assert.Equal(t, cleanUploadFileName(k), v)
+ assert.EqualValues(t, cleanUploadFileName(k), v)
}
}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index 403245596d..650b1d88f4 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -31,7 +31,6 @@ import (
"forgejo.org/modules/structs"
"forgejo.org/modules/util"
"forgejo.org/services/context"
- redirect_service "forgejo.org/services/redirect"
repo_service "forgejo.org/services/repository"
"github.com/go-chi/cors"
@@ -112,7 +111,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
return nil
}
- if redirectRepoID, err := redirect_service.LookupRepoRedirect(ctx, ctx.Doer, owner.ID, reponame); err == nil {
+ if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
context.RedirectToRepo(ctx.Base, redirectRepoID)
return nil
}
@@ -184,7 +183,9 @@ func httpBase(ctx *context.Context) *serviceHandler {
if repoExist {
// Because of special ref "refs/for" .. , need delay write permission check
- accessMode = perm.AccessModeRead
+ if git.SupportProcReceive {
+ accessMode = perm.AccessModeRead
+ }
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
@@ -193,19 +194,24 @@ func httpBase(ctx *context.Context) *serviceHandler {
ctx.ServerError("GetTaskByID", err)
return nil
}
-
- p, err := access_model.GetActionRepoPermission(ctx, repo, task)
- if err != nil {
- ctx.ServerError("GetActionRepoPermission", err)
- return nil
- }
-
- if !p.CanAccess(accessMode, unitType) {
+ if task.RepoID != repo.ID {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
- environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, p.AccessMode))
+ 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 {
diff --git a/routers/web/repo/githttp_test.go b/routers/web/repo/githttp_test.go
index 0164b11f66..5ba8de3d63 100644
--- a/routers/web/repo/githttp_test.go
+++ b/routers/web/repo/githttp_test.go
@@ -37,6 +37,6 @@ func TestContainsParentDirectorySeparator(t *testing.T) {
}
for i := range tests {
- assert.Equal(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
+ assert.EqualValues(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
}
}
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index 3401f2f666..9d67f142fb 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -35,7 +35,7 @@ func HandleGitError(ctx *context.Context, msg string, err error) {
case ctx.Repo.IsViewCommit:
refType = "commit"
}
- ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.tree_path_not_found."+refType, ctx.Repo.TreePath, url.PathEscape(ctx.Repo.RefName))
+ ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.tree_path_not_found_"+refType, ctx.Repo.TreePath, url.PathEscape(ctx.Repo.RefName))
ctx.Data["NotFoundGoBackURL"] = ctx.Repo.RepoLink + "/src/" + refType + "/" + url.PathEscape(ctx.Repo.RefName)
ctx.NotFound(msg, err)
} else {
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index f445ce4a1a..ff3a903aed 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -76,27 +76,34 @@ const (
issueTemplateTitleKey = "IssueTemplateTitle"
)
-// generateIssueTemplateLocations generates all the file paths where we
-// look for an issue template, e.g. ".forgejo/ISSUE_TEMPLATE.md".
-func generateIssueTemplateLocations() []string {
- var result []string
- prefixes := []string{"", ".forgejo/", ".gitea/", ".github/", "docs/"}
- filenames := []string{"ISSUE_TEMPLATE", "issue_template"}
- extensions := []string{".md", ".yaml", ".yml"}
-
- for _, prefix := range prefixes {
- for _, filename := range filenames {
- for _, extension := range extensions {
- result = append(result, prefix+filename+extension)
- }
- }
- }
-
- return result
+// IssueTemplateCandidates issue templates
+var IssueTemplateCandidates = []string{
+ "ISSUE_TEMPLATE.md",
+ "ISSUE_TEMPLATE.yaml",
+ "ISSUE_TEMPLATE.yml",
+ "issue_template.md",
+ "issue_template.yaml",
+ "issue_template.yml",
+ ".forgejo/ISSUE_TEMPLATE.md",
+ ".forgejo/ISSUE_TEMPLATE.yaml",
+ ".forgejo/ISSUE_TEMPLATE.yml",
+ ".forgejo/issue_template.md",
+ ".forgejo/issue_template.yaml",
+ ".forgejo/issue_template.yml",
+ ".gitea/ISSUE_TEMPLATE.md",
+ ".gitea/ISSUE_TEMPLATE.yaml",
+ ".gitea/ISSUE_TEMPLATE.yml",
+ ".gitea/issue_template.md",
+ ".gitea/issue_template.yaml",
+ ".gitea/issue_template.yml",
+ ".github/ISSUE_TEMPLATE.md",
+ ".github/ISSUE_TEMPLATE.yaml",
+ ".github/ISSUE_TEMPLATE.yml",
+ ".github/issue_template.md",
+ ".github/issue_template.yaml",
+ ".github/issue_template.yml",
}
-var issueTemplateCandidates = generateIssueTemplateLocations()
-
// MustAllowUserComment checks to make sure if an issue is locked.
// If locked and user has permissions to write to the repository,
// then the comment is allowed, else it is blocked
@@ -180,10 +187,9 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
// 0 means issues with no label
// blank means labels will not be filtered for issues
selectLabels := ctx.FormString("labels")
- switch selectLabels {
- case "":
+ if selectLabels == "" {
ctx.Data["AllLabels"] = true
- case "0":
+ } else if selectLabels == "0" {
ctx.Data["NoLabel"] = true
}
if len(selectLabels) > 0 {
@@ -341,6 +347,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.ServerError("GetIssuesAllCommitStatus", err)
return
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ for key := range commitStatuses {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
+ }
+ }
if err := issues.LoadAttributes(ctx); err != nil {
ctx.ServerError("issues.LoadAttributes", err)
@@ -415,10 +426,9 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- switch typ {
- case "reject":
+ if typ == "reject" {
reviewTyp = issues_model.ReviewTypeReject
- case "waiting":
+ } else if typ == "waiting" {
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
@@ -998,7 +1008,7 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Tags"] = tags
_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, issueTemplateCandidates)
+ templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
for k, v := range errs {
templateErrs[k] = v
}
@@ -1301,7 +1311,7 @@ func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *use
}
// Special user that can't have associated contributions and permissions in the repo.
- if poster.IsSystem() || poster.IsAPServerActor() {
+ if poster.IsGhost() || poster.IsActions() || poster.IsAPActor() {
return roleDescriptor, nil
}
@@ -1465,7 +1475,6 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IssueType"] = "all"
}
- ctx.Data["IsModerationEnabled"] = setting.Moderation.Enabled
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
@@ -1590,7 +1599,6 @@ func ViewIssue(ctx *context.Context) {
ok bool
marked = make(map[int64]issues_model.RoleDescriptor)
comment *issues_model.Comment
- commentIdx int
participants = make([]*user_model.User, 1, 10)
latestCloseCommentID int64
)
@@ -1646,17 +1654,15 @@ func ViewIssue(ctx *context.Context) {
return
}
- for commentIdx, comment = range issue.Comments {
+ for _, comment = range issue.Comments {
comment.Issue = issue
- metas := ctx.Repo.Repository.ComposeMetas(ctx)
- metas["scope"] = fmt.Sprintf("comment-%d", commentIdx)
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
- Metas: metas,
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content)
@@ -1689,7 +1695,7 @@ func ViewIssue(ctx *context.Context) {
return
}
ghostMilestone := &issues_model.Milestone{
- ID: issues_model.GhostMilestoneID,
+ ID: -1,
Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
}
if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
@@ -1733,7 +1739,7 @@ func ViewIssue(ctx *context.Context) {
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
- Metas: metas,
+ Metas: ctx.Repo.Repository.ComposeMetas(ctx),
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content)
@@ -1790,6 +1796,15 @@ func ViewIssue(ctx *context.Context) {
ctx.ServerError("LoadPushCommits", err)
return
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ for _, commit := range comment.Commits {
+ if commit.Status == nil {
+ continue
+ }
+ commit.Status.HideActionsURL(ctx)
+ git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
+ }
+ }
} else if comment.Type == issues_model.CommentTypeAddTimeManual ||
comment.Type == issues_model.CommentTypeStopTracking ||
comment.Type == issues_model.CommentTypeDeleteTimeManual {
@@ -2113,7 +2128,7 @@ func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.Depende
}
repoPerms[blocker.RepoID] = perm
}
- if perm.CanReadIssuesOrPulls(blocker.IsPull) {
+ if perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
canRead = append(canRead, blocker)
} else {
notPermitted = append(notPermitted, blocker)
@@ -2397,6 +2412,10 @@ func UpdateIssueMilestone(ctx *context.Context) {
}
if ctx.FormBool("htmx") {
+ renderMilestones(ctx)
+ if ctx.Written() {
+ return
+ }
prepareHiddenCommentType(ctx)
if ctx.Written() {
return
@@ -2410,7 +2429,6 @@ func UpdateIssueMilestone(ctx *context.Context) {
ctx.ServerError("GetMilestoneByRepoID", err)
return
}
- ctx.Data["OpenMilestones"] = true
} else {
issue.Milestone = nil
}
@@ -2768,7 +2786,7 @@ func SearchIssues(ctx *context.Context) {
IncludedAnyLabelIDs: includedAnyLabels,
MilestoneIDs: includedMilestones,
ProjectID: projectID,
- SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc),
+ SortBy: issue_indexer.SortByCreatedDesc,
}
if since != 0 {
@@ -2797,10 +2815,9 @@ func SearchIssues(ctx *context.Context) {
}
}
- priorityRepoID := ctx.FormInt64("priority_repo_id")
- if priorityRepoID > 0 {
- searchOpt.PriorityRepoID = optional.Some(priorityRepoID)
- }
+ // FIXME: It's unsupported to sort by priority repo when searching by indexer,
+ // it's indeed an regression, but I think it is worth to support filtering by indexer first.
+ _ = ctx.FormInt64("priority_repo_id")
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
if err != nil {
@@ -2938,7 +2955,7 @@ func ListIssues(ctx *context.Context) {
IsPull: isPull,
IsClosed: isClosed,
ProjectID: projectID,
- SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc),
+ SortBy: issue_indexer.SortByCreatedDesc,
}
if since != 0 {
searchOpt.UpdatedAfterUnix = optional.Some(since)
@@ -3100,7 +3117,7 @@ func NewComment(ctx *context.Context) {
// 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) {
+ !(issue.IsPull && issue.PullRequest.HasMerged) {
// Duplication and conflict check should apply to reopen pull request.
var pr *issues_model.PullRequest
@@ -3236,7 +3253,11 @@ func NewComment(ctx *context.Context) {
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
if err != nil {
if errors.Is(err, user_model.ErrBlockedByUser) {
- ctx.JSONError(ctx.Tr("repo.comment.blocked_by_user"))
+ if issue.IsPull {
+ ctx.JSONError(ctx.Tr("repo.pulls.comment.blocked_by_user"))
+ } else {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_by_user"))
+ }
} else {
ctx.ServerError("CreateIssueComment", err)
}
@@ -3586,9 +3607,9 @@ func GetIssueAttachments(ctx *context.Context) {
if ctx.Written() {
return
}
- attachments := make([]*api.WebAttachment, len(issue.Attachments))
+ attachments := make([]*api.Attachment, len(issue.Attachments))
for i := 0; i < len(issue.Attachments); i++ {
- attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, issue.Attachments[i])
+ attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i])
}
ctx.JSON(http.StatusOK, attachments)
}
@@ -3611,7 +3632,7 @@ func GetCommentAttachments(ctx *context.Context) {
return
}
- if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
+ if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{})
return
}
@@ -3621,13 +3642,13 @@ func GetCommentAttachments(ctx *context.Context) {
return
}
+ attachments := make([]*api.Attachment, 0)
if err := comment.LoadAttachments(ctx); err != nil {
ctx.ServerError("LoadAttachments", err)
return
}
- attachments := make([]*api.WebAttachment, len(comment.Attachments))
for i := 0; i < len(comment.Attachments); i++ {
- attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, comment.Attachments[i])
+ attachments = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i]))
}
ctx.JSON(http.StatusOK, attachments)
}
@@ -3777,49 +3798,3 @@ func issuePosters(ctx *context.Context, isPullList bool) {
}
ctx.JSON(http.StatusOK, resp)
}
-
-func getIssueParticipants(ctx *context.Context, issue *issues_model.Issue) []*user_model.User {
- var (
- participants = make([]*user_model.User, 1, 10)
- comment *issues_model.Comment
- )
-
- participants[0] = issue.Poster
-
- if err := issue.LoadComments(ctx); err != nil {
- ctx.ServerError("loadComments", err)
- return nil
- }
-
- if err := issue.Comments.LoadPosters(ctx); err != nil {
- ctx.ServerError("LoadPosters", err)
- return nil
- }
-
- for _, comment = range issue.Comments {
- if comment.Type == issues_model.CommentTypeComment ||
- comment.Type == issues_model.CommentTypeReview ||
- comment.Type == issues_model.CommentTypePullRequestPush {
- participants = addParticipant(comment.Poster, participants)
- } else if comment.Type.HasContentSupport() {
- participants = addParticipant(comment.Poster, participants)
-
- if comment.Review == nil {
- continue
- }
- if err := comment.Review.LoadCodeComments(ctx); err != nil {
- ctx.ServerError("Review.LoadCodeComments", err)
- return nil
- }
- for _, codeComments := range comment.Review.CodeComments {
- for _, lineComments := range codeComments {
- for _, c := range lineComments {
- participants = addParticipant(c.Poster, participants)
- }
- }
- }
- }
- }
-
- return participants
-}
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 11d0de90de..5c71d75f80 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -160,16 +160,15 @@ func GetContentHistoryDetail(ctx *context.Context) {
diffHTMLBuf := bytes.Buffer{}
diffHTMLBuf.WriteString("")
for _, it := range diff {
- switch it.Type {
- case diffmatchpatch.DiffInsert:
+ if it.Type == diffmatchpatch.DiffInsert {
diffHTMLBuf.WriteString("")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString(" ")
- case diffmatchpatch.DiffDelete:
+ } else if it.Type == diffmatchpatch.DiffDelete {
diffHTMLBuf.WriteString("")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString(" ")
- default:
+ } else {
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
}
}
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 0adcc39499..406ab4918c 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -39,7 +39,7 @@ func TestInitializeLabels(t *testing.T) {
contexttest.LoadRepo(t, ctx, 2)
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
RepoID: 2,
Name: "enhancement",
@@ -69,7 +69,7 @@ func TestRetrieveLabels(t *testing.T) {
assert.True(t, ok)
if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
for i, label := range labels {
- assert.Equal(t, testCase.ExpectedLabelIDs[i], label.ID)
+ assert.EqualValues(t, testCase.ExpectedLabelIDs[i], label.ID)
}
}
}
@@ -85,7 +85,7 @@ func TestNewLabel(t *testing.T) {
Color: "#abcdef",
})
NewLabel(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
Name: "newlabel",
Color: "#abcdef",
@@ -105,7 +105,7 @@ func TestUpdateLabel(t *testing.T) {
IsArchived: true,
})
UpdateLabel(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
ID: 2,
Name: "newnameforlabel",
@@ -121,7 +121,7 @@ func TestDeleteLabel(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("id", "2")
DeleteLabel(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
@@ -135,7 +135,7 @@ func TestUpdateIssueLabel_Clear(t *testing.T) {
ctx.Req.Form.Set("issue_ids", "1,3")
ctx.Req.Form.Set("action", "clear")
UpdateIssueLabel(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
unittest.CheckConsistencyFor(t, &issues_model.Label{})
@@ -161,7 +161,7 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
ctx.Req.Form.Set("action", testCase.Action)
ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
UpdateIssueLabel(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
for _, issueID := range testCase.IssueIDs {
unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{
IssueID: issueID,
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index 3a5cf30dbe..86d2461e94 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -138,8 +138,6 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl base.TplN
}
case addrErr.IsInvalidPath:
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
- case addrErr.HasCredentials:
- ctx.RenderWithErr(ctx.Tr("migrate.form.error.url_credentials"), tpl, form)
default:
log.Error("Error whilst updating url: %v", err)
ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index e5bd06e987..80f699787c 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -120,7 +120,7 @@ func Projects(ctx *context.Context) {
pager.AddParam(ctx, "state", "State")
ctx.Data["Page"] = pager
- ctx.Data["CanWriteProjects"] = ctx.Repo.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["IsShowClosed"] = isShowClosed
ctx.Data["IsProjectsPage"] = true
ctx.Data["SortType"] = sortType
@@ -146,7 +146,7 @@ func RenderNewProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
ctx.Data["TemplateConfigs"] = project_model.GetTemplateConfigs()
ctx.Data["CardTypes"] = project_model.GetCardConfig()
- ctx.Data["CanWriteProjects"] = ctx.Repo.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CancelLink"] = ctx.Repo.Repository.Link() + "/projects"
ctx.HTML(http.StatusOK, tplProjectsNew)
}
@@ -228,7 +228,7 @@ func DeleteProject(ctx *context.Context) {
func RenderEditProject(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsEditProjects"] = true
- ctx.Data["CanWriteProjects"] = ctx.Repo.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CardTypes"] = project_model.GetCardConfig()
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
@@ -262,7 +262,7 @@ func EditProjectPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
ctx.Data["PageIsEditProjects"] = true
- ctx.Data["CanWriteProjects"] = ctx.Repo.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID)
@@ -378,7 +378,7 @@ func ViewProject(ctx *context.Context) {
ctx.Data["Title"] = project.Title
ctx.Data["IsProjectsPage"] = true
- ctx.Data["CanWriteProjects"] = ctx.Repo.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["Project"] = project
ctx.Data["IssuesMap"] = issuesMap
ctx.Data["Columns"] = columns
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index e484187b54..a54a31ac36 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -10,16 +10,14 @@ import (
"errors"
"fmt"
"html"
- "html/template"
"net/http"
"net/url"
- "path"
"strconv"
"strings"
+ "time"
"forgejo.org/models"
activities_model "forgejo.org/models/activities"
- asymkey_model "forgejo.org/models/asymkey"
"forgejo.org/models/db"
git_model "forgejo.org/models/git"
issues_model "forgejo.org/models/issues"
@@ -31,13 +29,11 @@ import (
"forgejo.org/models/unit"
user_model "forgejo.org/models/user"
"forgejo.org/modules/base"
- "forgejo.org/modules/charset"
"forgejo.org/modules/emoji"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
issue_template "forgejo.org/modules/issue/template"
"forgejo.org/modules/log"
- "forgejo.org/modules/markup"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
"forgejo.org/modules/structs"
@@ -66,27 +62,33 @@ const (
pullRequestTemplateKey = "PullRequestTemplate"
)
-// generatePullRequestTemplateLocations generates all the file paths where we
-// look for a pull request template, e.g. ".forgejo/PULL_REQUEST_TEMPLATE.md".
-func generatePullRequestTemplateLocations() []string {
- var result []string
- prefixes := []string{"", ".forgejo/", ".gitea/", ".github/", "docs/"}
- filenames := []string{"PULL_REQUEST_TEMPLATE", "pull_request_template"}
- extensions := []string{".md", ".yaml", ".yml"}
-
- for _, prefix := range prefixes {
- for _, filename := range filenames {
- for _, extension := range extensions {
- result = append(result, prefix+filename+extension)
- }
- }
- }
-
- return result
+var pullRequestTemplateCandidates = []string{
+ "PULL_REQUEST_TEMPLATE.md",
+ "PULL_REQUEST_TEMPLATE.yaml",
+ "PULL_REQUEST_TEMPLATE.yml",
+ "pull_request_template.md",
+ "pull_request_template.yaml",
+ "pull_request_template.yml",
+ ".forgejo/PULL_REQUEST_TEMPLATE.md",
+ ".forgejo/PULL_REQUEST_TEMPLATE.yaml",
+ ".forgejo/PULL_REQUEST_TEMPLATE.yml",
+ ".forgejo/pull_request_template.md",
+ ".forgejo/pull_request_template.yaml",
+ ".forgejo/pull_request_template.yml",
+ ".gitea/PULL_REQUEST_TEMPLATE.md",
+ ".gitea/PULL_REQUEST_TEMPLATE.yaml",
+ ".gitea/PULL_REQUEST_TEMPLATE.yml",
+ ".gitea/pull_request_template.md",
+ ".gitea/pull_request_template.yaml",
+ ".gitea/pull_request_template.yml",
+ ".github/PULL_REQUEST_TEMPLATE.md",
+ ".github/PULL_REQUEST_TEMPLATE.yaml",
+ ".github/PULL_REQUEST_TEMPLATE.yml",
+ ".github/pull_request_template.md",
+ ".github/pull_request_template.yaml",
+ ".github/pull_request_template.yml",
}
-var pullRequestTemplateCandidates = generatePullRequestTemplateLocations()
-
func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
@@ -354,7 +356,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
ctx.Data["Issue"] = issue
if !issue.IsPull {
- ctx.Redirect(issue.Link())
+ ctx.NotFound("ViewPullCommits", nil)
return nil, false
}
@@ -399,7 +401,6 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
// GetPullDiffStats get Pull Requests diff stats
func GetPullDiffStats(ctx *context.Context) {
- // FIXME: this getPullInfo seems to be a duplicate call with other route handlers
issue, ok := getPullInfo(ctx)
if !ok {
return
@@ -407,15 +408,15 @@ func GetPullDiffStats(ctx *context.Context) {
pull := issue.PullRequest
mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
+
if mergeBaseCommitID == "" {
ctx.NotFound("PullFiles", nil)
return
}
- // do not report 500 server error to end users if error occurs, otherwise a PR missing ref won't be able to view.
headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
- log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
+ ctx.ServerError("GetRefCommitID", err)
return
}
@@ -497,7 +498,6 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
- ctx.Data["CommitIDs"] = map[string]bool{}
ctx.Data["NumFiles"] = 0
return nil
}
@@ -508,12 +508,6 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
ctx.Data["NumCommits"] = len(compareInfo.Commits)
ctx.Data["NumFiles"] = compareInfo.NumFiles
- commitIDs := map[string]bool{}
- for _, commit := range compareInfo.Commits {
- commitIDs[commit.ID.String()] = true
- }
- ctx.Data["CommitIDs"] = commitIDs
-
if len(compareInfo.Commits) != 0 {
sha := compareInfo.Commits[0].ID.String()
commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
@@ -521,6 +515,9 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
ctx.ServerError("GetLatestCommitStatus", err)
return nil
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
+ }
if len(commitStatuses) != 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
@@ -584,6 +581,9 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.ServerError("GetLatestCommitStatus", err)
return nil
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
+ }
if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
@@ -597,7 +597,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
- ctx.Data["CommitIDs"] = map[string]bool{}
ctx.Data["NumFiles"] = 0
return nil
}
@@ -608,13 +607,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["NumCommits"] = len(compareInfo.Commits)
ctx.Data["NumFiles"] = compareInfo.NumFiles
-
- commitIDs := map[string]bool{}
- for _, commit := range compareInfo.Commits {
- commitIDs[commit.ID.String()] = true
- }
- ctx.Data["CommitIDs"] = commitIDs
-
return compareInfo
}
@@ -632,7 +624,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
if pull.Flow == issues_model.PullRequestFlowGithub {
headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
} else {
- headBranchExist = baseGitRepo.IsReferenceExist(pull.GetGitRefName())
+ headBranchExist = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName())
}
if headBranchExist {
@@ -673,7 +665,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
}
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
- ctx.Data["CommitIDs"] = map[string]bool{}
ctx.Data["NumFiles"] = 0
return nil
}
@@ -686,6 +677,9 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.ServerError("GetLatestCommitStatus", err)
return nil
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
+ }
if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
@@ -733,7 +727,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["HeadBranchCommitID"] = headBranchSha
ctx.Data["PullHeadCommitID"] = sha
- if pull.HeadRepo == nil || !headBranchExist || (!pull.Issue.IsClosed && !pull.IsChecking() && (headBranchSha != sha)) {
+ if pull.HeadRepo == nil || !headBranchExist || (!pull.Issue.IsClosed && (headBranchSha != sha)) {
ctx.Data["IsPullRequestBroken"] = true
if pull.IsSameRepo() {
ctx.Data["HeadTarget"] = pull.HeadBranch
@@ -751,7 +745,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
- ctx.Data["CommitIDs"] = map[string]bool{}
ctx.Data["NumFiles"] = 0
return nil
}
@@ -776,13 +769,6 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.Data["NumCommits"] = len(compareInfo.Commits)
ctx.Data["NumFiles"] = compareInfo.NumFiles
-
- commitIDs := map[string]bool{}
- for _, commit := range compareInfo.Commits {
- commitIDs[commit.ID.String()] = true
- }
- ctx.Data["CommitIDs"] = commitIDs
-
return compareInfo
}
@@ -861,7 +847,7 @@ func ViewPullCommits(ctx *context.Context) {
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- commits := git_model.ParseCommitsWithStatus(ctx, prInfo.Commits, ctx.Repo.Repository)
+ commits := processGitCommits(ctx, prInfo.Commits)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
@@ -906,7 +892,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
foundStartCommit := len(specifiedStartCommit) == 0
foundEndCommit := len(specifiedEndCommit) == 0
- if !foundStartCommit || !foundEndCommit {
+ if !(foundStartCommit && foundEndCommit) {
for _, commit := range prInfo.Commits {
if commit.ID.String() == specifiedStartCommit {
foundStartCommit = true
@@ -921,7 +907,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
}
- if !foundStartCommit || !foundEndCommit {
+ if !(foundStartCommit && foundEndCommit) {
ctx.NotFound("Given SHA1 not found for this PR", nil)
return
}
@@ -942,85 +928,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit
- if willShowSpecifiedCommit {
- commitID := specifiedEndCommit
-
- ctx.Data["CommitID"] = commitID
-
- var prevCommit, curCommit, nextCommit *git.Commit
-
- // Iterate in reverse to properly map "previous" and "next" buttons
- for i := len(prInfo.Commits) - 1; i >= 0; i-- {
- commit := prInfo.Commits[i]
-
- if curCommit != nil {
- nextCommit = commit
- break
- }
-
- if commit.ID.String() == commitID {
- curCommit = commit
- } else {
- prevCommit = commit
- }
- }
-
- if curCommit == nil {
- ctx.ServerError("Repo.GitRepo.viewPullFiles", git.ErrNotExist{ID: commitID})
- return
- }
-
- ctx.Data["Commit"] = curCommit
- if prevCommit != nil {
- ctx.Data["PrevCommitLink"] = path.Join(ctx.Repo.RepoLink, "pulls", strconv.FormatInt(issue.Index, 10), "commits", prevCommit.ID.String())
- }
- if nextCommit != nil {
- ctx.Data["NextCommitLink"] = path.Join(ctx.Repo.RepoLink, "pulls", strconv.FormatInt(issue.Index, 10), "commits", nextCommit.ID.String())
- }
-
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
- if err != nil {
- log.Error("GetLatestCommitStatus: %v", err)
- }
-
- ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses)
- ctx.Data["CommitStatuses"] = statuses
-
- verification := asymkey_model.ParseCommitWithSignature(ctx, curCommit)
- ctx.Data["Verification"] = verification
- ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, curCommit)
-
- if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
- return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
- }, nil); err != nil {
- ctx.ServerError("CalculateTrustStatus", err)
- return
- }
-
- note := &git.Note{}
- err = git.GetNote(ctx, ctx.Repo.GitRepo, specifiedEndCommit, note)
- if err == nil {
- ctx.Data["NoteCommit"] = note.Commit
- ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
- ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{
- Links: markup.Links{
- Base: ctx.Repo.RepoLink,
- BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)),
- },
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
- GitRepo: ctx.Repo.GitRepo,
- Ctx: ctx,
- }, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
- if err != nil {
- ctx.ServerError("RenderCommitMessage", err)
- return
- }
- }
-
- endCommitID = commitID
- startCommitID = prInfo.MergeBase
- ctx.Data["IsShowingAllCommits"] = false
- } else if willShowSpecifiedCommitRange {
+ if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
if len(specifiedEndCommit) > 0 {
endCommitID = specifiedEndCommit
} else {
@@ -1031,7 +939,6 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
} else {
startCommitID = prInfo.MergeBase
}
-
ctx.Data["IsShowingAllCommits"] = false
} else {
endCommitID = headCommitID
@@ -1039,10 +946,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["IsShowingAllCommits"] = true
}
- ctx.Data["AfterCommitID"] = endCommitID
- ctx.Data["BeforeCommitID"] = startCommitID
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
+ ctx.Data["AfterCommitID"] = endCommitID
+ ctx.Data["BeforeCommitID"] = startCommitID
fileOnly := ctx.FormBool("file-only")
@@ -1074,7 +981,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
// as the viewed information is designed to be loaded only on latest PR
// diff and if you're signed in.
if !ctx.IsSigned || willShowSpecifiedCommit || willShowSpecifiedCommitRange {
- diff, err = gitdiff.GetDiffFull(ctx, gitRepo, diffOptions, files...)
+ diff, err = gitdiff.GetDiff(ctx, gitRepo, diffOptions, files...)
methodWithError = "GetDiff"
} else {
diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...)
@@ -1146,7 +1053,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.ServerError("GetUserRepoPermission", err)
return
}
- ctx.Data["HeadBranchIsEditable"] = pull.HeadRepo.CanEnableEditor() && issues_model.CanMaintainerWriteToBranch(ctx, headRepoPerm, pull.HeadBranch, ctx.Doer) && pull.Flow != issues_model.PullRequestFlowAGit
+ ctx.Data["HeadBranchIsEditable"] = pull.HeadRepo.CanEnableEditor() && issues_model.CanMaintainerWriteToBranch(ctx, headRepoPerm, pull.HeadBranch, ctx.Doer)
ctx.Data["SourceRepoLink"] = pull.HeadRepo.Link()
ctx.Data["HeadBranch"] = pull.HeadBranch
}
@@ -1167,13 +1074,6 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
- participants := getIssueParticipants(ctx, issue)
- if ctx.Written() {
- return
- }
- ctx.Data["Participants"] = participants
- ctx.Data["NumParticipants"] = len(participants)
-
handleTeamMentions(ctx)
if ctx.Written() {
return
@@ -1307,6 +1207,8 @@ func UpdatePullRequest(ctx *context.Context) {
return
}
+ time.Sleep(1 * time.Second)
+
ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success"))
ctx.Redirect(issue.Link())
}
@@ -1419,8 +1321,8 @@ func MergePullRequest(ctx *context.Context) {
} else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.pulls.merge_conflict"),
- "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
+ "Message": ctx.Tr("repo.editor.merge_conflict"),
+ "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + " " + utils.SanitizeFlashErrorString(conflictError.StdOut),
})
if err != nil {
@@ -1446,10 +1348,6 @@ func MergePullRequest(ctx *context.Context) {
log.Debug("MergeUnrelatedHistories error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
ctx.JSONRedirect(issue.Link())
- } else if models.IsErrPullRequestHasMerged(err) {
- log.Debug("MergePullRequestHasMerged error: %v", err)
- ctx.Flash.Error(ctx.Tr("repo.pulls.already_merged"))
- ctx.JSONRedirect(issue.Link())
} else if git.IsErrPushOutOfDate(err) {
log.Debug("MergePushOutOfDate error: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 941e428039..18a5b872f1 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -211,10 +211,9 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
- switch origin {
- case "diff":
+ if origin == "diff" {
ctx.HTML(http.StatusOK, tplDiffConversation)
- case "timeline":
+ } else if origin == "timeline" {
ctx.HTML(http.StatusOK, tplTimelineConversation)
}
}
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
index 211b1b2b12..6154de7377 100644
--- a/routers/web/repo/recent_commits.go
+++ b/routers/web/repo/recent_commits.go
@@ -4,10 +4,12 @@
package repo
import (
+ "errors"
"net/http"
"forgejo.org/modules/base"
"forgejo.org/services/context"
+ contributors_service "forgejo.org/services/repository"
)
const (
@@ -24,3 +26,16 @@ func RecentCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRecentCommits)
}
+
+// RecentCommitsData returns JSON of recent commits data
+func RecentCommitsData(ctx *context.Context) {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if errors.Is(err, contributors_service.ErrAwaitGeneration) {
+ ctx.Status(http.StatusAccepted)
+ return
+ }
+ ctx.ServerError("RecentCommitsData", err)
+ } else {
+ ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
+ }
+}
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index a6de337192..024dd7b62d 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
-// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
@@ -250,7 +249,7 @@ func addVerifyTagToContext(ctx *context.Context) {
if verification == nil {
return false
}
- return verification.Reason != asymkey.NotSigned
+ return verification.Reason != "gpg.error.not_signed_commit"
}
}
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index 05eeadc519..b31e2e203a 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -41,7 +41,7 @@ func RenderFile(ctx *context.Context) {
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
- st := typesniffer.DetectContentType(buf, blob.Name())
+ st := typesniffer.DetectContentType(buf)
isTextFile := st.IsText()
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 493787ad8b..53b3f34347 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -693,6 +693,9 @@ func SearchRepo(ctx *context.Context) {
ctx.JSON(http.StatusInternalServerError, nil)
return
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ git_model.CommitStatusesHideActionsURL(ctx, latestCommitStatuses)
+ }
results := make([]*repo_service.WebSearchRepository, len(repos))
for i, repo := range repos {
@@ -779,27 +782,3 @@ func PrepareBranchList(ctx *context.Context) {
}
ctx.Data["Branches"] = brs
}
-
-func SyncFork(ctx *context.Context) {
- redirectURL := fmt.Sprintf("%s/src/branch/%s", ctx.Repo.RepoLink, util.PathEscapeSegments(ctx.Repo.BranchName))
- branch := ctx.FormString("branch")
-
- syncForkInfo, err := repo_service.GetSyncForkInfo(ctx, ctx.Repo.Repository, branch)
- if err != nil {
- ctx.ServerError("GetSyncForkInfo", err)
- return
- }
-
- if !syncForkInfo.Allowed {
- ctx.Redirect(redirectURL)
- return
- }
-
- err = repo_service.SyncFork(ctx, ctx.Doer, ctx.Repo.Repository, branch)
- if err != nil {
- ctx.ServerError("SyncFork", err)
- return
- }
-
- ctx.Redirect(redirectURL)
-}
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index ad10542c01..1671378a3b 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -165,8 +165,6 @@ func Search(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "l", "Language")
- pager.AddParam(ctx, "mode", "CodeSearchMode")
- pager.AddParam(ctx, "path", "CodeSearchPath")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplSearch)
diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go
index 20e211316d..abbb12cacb 100644
--- a/routers/web/repo/setting/avatar.go
+++ b/routers/web/repo/setting/avatar.go
@@ -45,8 +45,8 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
if err != nil {
return fmt.Errorf("io.ReadAll: %w", err)
}
- st := typesniffer.DetectContentType(data, "")
- if !st.IsImage() || st.IsSvgImage() {
+ st := typesniffer.DetectContentType(data)
+ if !(st.IsImage() && !st.IsSvgImage()) {
return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
}
if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil {
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 9930d03e8e..2e9c34e8a7 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -291,7 +291,7 @@ func LFSFileGet(ctx *context.Context) {
}
buf = buf[:n]
- st := typesniffer.DetectContentType(buf, "")
+ st := typesniffer.DetectContentType(buf)
ctx.Data["IsTextFile"] = st.IsText()
isRepresentableAsText := st.IsRepresentableAsText()
@@ -342,20 +342,6 @@ func LFSFileGet(ctx *context.Context) {
ctx.Data["IsVideoFile"] = true
case st.IsAudio():
ctx.Data["IsAudioFile"] = true
- case st.Is3DModel():
- ctx.Data["Is3DModelFile"] = true
- switch {
- case st.IsGLB():
- ctx.Data["IsGLBFile"] = true
- case st.IsSTL():
- ctx.Data["IsSTLFile"] = true
- case st.IsGLTF():
- ctx.Data["IsGLTFFile"] = true
- case st.IsOBJ():
- ctx.Data["IsOBJFile"] = true
- case st.Is3MF():
- ctx.Data["Is3MFFile"] = true
- }
case st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage()):
ctx.Data["IsImageFile"] = true
}
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 1f54ba353d..083cc4ae82 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -6,7 +6,6 @@
package setting
import (
- go_context "context"
"errors"
"fmt"
"net/http"
@@ -15,6 +14,7 @@ import (
"time"
"forgejo.org/models"
+ actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
"forgejo.org/models/organization"
quota_model "forgejo.org/models/quota"
@@ -24,7 +24,6 @@ import (
"forgejo.org/modules/base"
"forgejo.org/modules/git"
"forgejo.org/modules/indexer/code"
- "forgejo.org/modules/indexer/issues"
"forgejo.org/modules/indexer/stats"
"forgejo.org/modules/lfs"
"forgejo.org/modules/log"
@@ -65,9 +64,6 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
- ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize
- ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth
- ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
ctx.Data["SigningKeyAvailable"] = len(signing) > 0
@@ -154,9 +150,11 @@ func UnitsPost(ctx *context.Context) {
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
- wikiPermissions := repo_model.UnitAccessModeUnset
+ var wikiPermissions repo_model.UnitAccessMode
if form.GloballyWriteableWiki {
wikiPermissions = repo_model.UnitAccessModeWrite
+ } else {
+ wikiPermissions = repo_model.UnitAccessModeRead
}
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
@@ -543,13 +541,7 @@ func SettingsPost(ctx *context.Context) {
mirror_service.AddPullMirrorToQueue(repo.ID)
- sanitizedOriginalURL, err := util.SanitizeURL(repo.OriginalURL)
- if err != nil {
- ctx.ServerError("SanitizeURL", err)
- return
- }
-
- ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", sanitizedOriginalURL))
+ ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
ctx.Redirect(repo.Link() + "/settings")
case "push-mirror-sync":
@@ -596,23 +588,6 @@ func SettingsPost(ctx *context.Context) {
ctx.ServerError("UpdatePushMirrorInterval", err)
return
}
-
- if m.BranchFilter != form.PushMirrorBranchFilter {
- // replace `remote..push` in config and db
- m.BranchFilter = form.PushMirrorBranchFilter
- if err := db.WithTx(ctx, func(ctx go_context.Context) error {
- // Update the DB
- if err = repo_model.UpdatePushMirrorBranchFilter(ctx, m); err != nil {
- return err
- }
- // Update the repo config
- return mirror_service.UpdatePushMirrorBranchFilter(ctx, m)
- }); err != nil {
- ctx.ServerError("UpdatePushMirrorBranchFilter", err)
- return
- }
- }
-
// Background why we are adding it to Queue
// If we observed its implementation in the context of `push-mirror-sync` where it
// is evident that pushing to the queue is necessary for updates.
@@ -708,7 +683,6 @@ func SettingsPost(ctx *context.Context) {
SyncOnCommit: form.PushMirrorSyncOnCommit,
Interval: interval,
RemoteAddress: remoteAddress,
- BranchFilter: form.PushMirrorBranchFilter,
}
var plainPrivateKey []byte
@@ -802,8 +776,6 @@ func SettingsPost(ctx *context.Context) {
return
}
code.UpdateRepoIndexer(ctx.Repo.Repository)
- case "issues":
- issues.UpdateRepoIndexer(ctx, ctx.Repo.Repository.ID)
default:
ctx.NotFound("", nil)
return
@@ -828,9 +800,13 @@ func SettingsPost(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
return
}
+ repo.IsMirror = false
- if err := repo_service.ConvertMirrorToNormalRepo(ctx, ctx.Repo.Repository); err != nil {
- ctx.ServerError("ConvertMirror", err)
+ if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
+ ctx.ServerError("CleanUpMigrateInfo", err)
+ return
+ } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
+ ctx.ServerError("DeleteMirrorByRepoID", err)
return
}
log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
@@ -1058,7 +1034,7 @@ func SettingsPost(ctx *context.Context) {
return
}
- if err := actions_service.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
+ if err := actions_model.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
@@ -1112,8 +1088,6 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
}
case addrErr.IsInvalidPath:
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
- case addrErr.HasCredentials:
- ctx.RenderWithErr(ctx.Tr("migrate.form.error.url_credentials"), tplSettingsOptions, form)
default:
ctx.ServerError("Unknown error", err)
}
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index 3a81b85e4c..6f05953bfb 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -15,7 +15,6 @@ import (
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/setting"
- "forgejo.org/modules/test"
"forgejo.org/modules/web"
"forgejo.org/services/context"
"forgejo.org/services/contexttest"
@@ -26,8 +25,23 @@ import (
"github.com/stretchr/testify/require"
)
+func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
+ tmpDir := t.TempDir()
+
+ oldPath := setting.SSH.RootPath
+ setting.SSH.RootPath = tmpDir
+
+ return func() {
+ setting.SSH.RootPath = oldPath
+ }
+}
+
func TestAddReadOnlyDeployKey(t *testing.T) {
- defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
+ if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
+ defer deferable()
+ } else {
+ return
+ }
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys")
@@ -41,7 +55,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -51,7 +65,11 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
- defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
+ if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
+ defer deferable()
+ } else {
+ return
+ }
unittest.PrepareTestEnv(t)
@@ -67,7 +85,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -106,7 +124,7 @@ func TestCollaborationPost(t *testing.T) {
CollaborationPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
require.NoError(t, err)
@@ -132,7 +150,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) {
CollaborationPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -166,7 +184,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
CollaborationPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
require.NoError(t, err)
@@ -175,7 +193,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
// Try adding the same collaborator again
CollaborationPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -197,7 +215,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
CollaborationPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -237,7 +255,7 @@ func TestAddTeamPost(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.Empty(t, ctx.Flash.ErrorMsg)
}
@@ -277,7 +295,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) {
AddTeamPost(ctx)
assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -318,7 +336,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -351,7 +369,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) {
ctx.Repo = repo
AddTeamPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index 0caa196e25..6d4d9e47e2 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -175,9 +175,6 @@ func ParseHookEvent(form forms.WebhookCoreForm) *webhook_module.HookEvent {
Wiki: form.Wiki,
Repository: form.Repository,
Package: form.Package,
- ActionRunFailure: form.ActionFailure,
- ActionRunRecover: form.ActionRecover,
- ActionRunSuccess: form.ActionSuccess,
},
BranchFilter: form.BranchFilter,
}
diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go
index 20ea9babbe..5c37f2ebca 100644
--- a/routers/web/repo/treelist.go
+++ b/routers/web/repo/treelist.go
@@ -42,7 +42,7 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
return true
}
- if entry.IsSubmodule() {
+ if entry.IsSubModule() {
return true
}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 3b11d73390..bea002f690 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -1,6 +1,5 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
-// Copyright 2023 The Forgejo Authors. All rights reserved.
+// Copyright 2014 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
@@ -53,10 +52,9 @@ import (
"forgejo.org/routers/web/feed"
"forgejo.org/services/context"
issue_service "forgejo.org/services/issue"
- repo_service "forgejo.org/services/repository"
files_service "forgejo.org/services/repository/files"
- "code.forgejo.org/forgejo/runner/v11/act/model"
+ "github.com/nektos/act/pkg/model"
_ "golang.org/x/image/bmp" // for processing bmp images
_ "golang.org/x/image/webp" // for processing webp images
@@ -228,7 +226,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
- st := typesniffer.DetectContentType(buf, blob.Name())
+ st := typesniffer.DetectContentType(buf)
isTextFile := st.IsText()
// FIXME: what happens when README file is an image?
@@ -262,7 +260,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
}
buf = buf[:n]
- st = typesniffer.DetectContentType(buf, blob.Name())
+ st = typesniffer.DetectContentType(buf)
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
}
@@ -369,6 +367,9 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
+ if !ctx.Repo.CanRead(unit_model.TypeActions) {
+ git_model.CommitStatusesHideActionsURL(ctx, statuses)
+ }
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(statuses)
ctx.Data["LatestCommitStatuses"] = statuses
@@ -434,13 +435,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
if err != nil {
log.Error("actions.GetContentFromEntry: %v", err)
}
- _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content), true)
+ _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
if workFlowErr != nil {
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
}
- } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS", ".forgejo/CODEOWNERS"}, ctx.Repo.TreePath) {
- if rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err == nil {
- _, warnings := issue_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize)
+ } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
+ if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
+ _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
if len(warnings) > 0 {
ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
}
@@ -624,20 +625,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
ctx.Data["IsVideoFile"] = true
case fInfo.st.IsAudio():
ctx.Data["IsAudioFile"] = true
- case fInfo.st.Is3DModel():
- ctx.Data["Is3DModelFile"] = true
- switch {
- case fInfo.st.IsGLB():
- ctx.Data["IsGLBFile"] = true
- case fInfo.st.IsSTL():
- ctx.Data["IsSTLFile"] = true
- case fInfo.st.IsGLTF():
- ctx.Data["IsGLTFFile"] = true
- case fInfo.st.IsOBJ():
- ctx.Data["IsOBJFile"] = true
- case fInfo.st.Is3MF():
- ctx.Data["Is3MFFile"] = true
- }
case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
ctx.Data["IsImageFile"] = true
ctx.Data["CanCopyContent"] = true
@@ -1057,13 +1044,14 @@ func renderHomeCode(ctx *context.Context) {
return
}
- if entry.IsSubmodule() {
- submodule, err := ctx.Repo.Commit.GetSubmodule(ctx.Repo.TreePath, entry)
+ if entry.IsSubModule() {
+ subModuleURL, err := ctx.Repo.Commit.GetSubModule(entry.Name())
if err != nil {
- HandleGitError(ctx, "Repo.Commit.GetSubmodule", err)
+ HandleGitError(ctx, "Repo.Commit.GetSubModule", err)
return
}
- ctx.Redirect(submodule.ResolveUpstreamURL(ctx.Repo.Repository.HTMLURL()))
+ subModuleFile := git.NewSubModuleFile(ctx.Repo.Commit, subModuleURL, entry.ID.String())
+ ctx.Redirect(subModuleFile.RefURL(setting.AppURL, ctx.Repo.Repository.FullName(), setting.SSH.Domain))
} else if entry.IsDir() {
renderDirectory(ctx)
} else {
@@ -1166,20 +1154,6 @@ PostRecentBranchCheck:
}
}
- if ctx.Repo.Repository.IsFork && ctx.Repo.IsViewBranch && len(ctx.Repo.TreePath) == 0 && ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
- syncForkInfo, err := repo_service.GetSyncForkInfo(ctx, ctx.Repo.Repository, ctx.Repo.BranchName)
- if err != nil {
- ctx.ServerError("CanSync", err)
- return
- }
-
- if syncForkInfo.Allowed {
- ctx.Data["CanSyncFork"] = true
- ctx.Data["ForkCommitsBehind"] = syncForkInfo.CommitsBehind
- ctx.Data["BaseBranchLink"] = fmt.Sprintf("%s/src/branch/%s", ctx.Repo.Repository.BaseRepo.HTMLURL(), util.PathEscapeSegments(ctx.Repo.BranchName))
- }
- }
-
ctx.Data["Paths"] = paths
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
@@ -1221,7 +1195,7 @@ func checkOutdatedBranch(ctx *context.Context) {
}
if dbBranch.CommitID != commit.ID.String() {
- ctx.Flash.Warning(ctx.Tr("warning.repository.out_of_sync"), true)
+ ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
}
}
@@ -1251,7 +1225,6 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOp
func Watchers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.watchers")
ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
- ctx.Data["CardsNoneMsg"] = ctx.Tr("watch.list.none")
ctx.Data["PageIsWatchers"] = true
RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) {
@@ -1263,7 +1236,6 @@ func Watchers(ctx *context.Context) {
func Stars(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.stargazers")
ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
- ctx.Data["CardsNoneMsg"] = ctx.Tr("stars.list.none")
ctx.Data["PageIsStargazers"] = true
RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 1b5265978a..9a21ac21a3 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -393,7 +393,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
ctx.ServerError("CommitsByFileAndRange", err)
return nil, nil
}
- ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commitsHistory, ctx.Repo.Repository)
+ ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager.SetDefaultParams(ctx)
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index 5709b32257..cba416fc92 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -73,7 +73,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) {
return
}
for i, pageMeta := range pageMetas {
- assert.Equal(t, expectedNames[i], pageMeta.Name)
+ assert.EqualValues(t, expectedNames[i], pageMeta.Name)
}
}
@@ -84,7 +84,7 @@ func TestWiki(t *testing.T) {
ctx.SetParams("*", "Home")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assertPagesMetas(t, []string{"Home", "Long Page", "Page With Image", "Page With Spaced Name", "Unescaped File", "XSS"}, ctx.Data["Pages"])
}
@@ -95,7 +95,7 @@ func TestWikiPages(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
contexttest.LoadRepo(t, ctx, 1)
WikiPages(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assertPagesMetas(t, []string{"Home", "Long Page", "Page With Image", "Page With Spaced Name", "Unescaped File", "XSS"}, ctx.Data["Pages"])
}
@@ -106,7 +106,7 @@ func TestNewWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
NewWiki(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
}
@@ -126,7 +126,7 @@ func TestNewWikiPost(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
}
@@ -144,7 +144,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
}
@@ -157,7 +157,7 @@ func TestEditWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
}
@@ -178,7 +178,7 @@ func TestEditWikiPost(t *testing.T) {
Message: message,
})
EditWikiPost(ctx)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
if title != "Home" {
@@ -194,7 +194,7 @@ func TestDeleteWikiPagePost(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
DeleteWikiPagePost(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assertWikiNotExists(t, ctx.Repo.Repository, "Home")
}
@@ -215,10 +215,10 @@ func TestWikiRaw(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1)
WikiRaw(ctx)
if filetype == "" {
- assert.Equal(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath)
+ assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath)
} else {
- assert.Equal(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath)
- assert.Equal(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath)
+ assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
}
}
}
diff --git a/routers/web/shared/actions/fixtures/TestRunnerDetails/action_runner.yml b/routers/web/shared/actions/fixtures/TestRunnerDetails/action_runner.yml
deleted file mode 100644
index d783f83110..0000000000
--- a/routers/web/shared/actions/fixtures/TestRunnerDetails/action_runner.yml
+++ /dev/null
@@ -1,7 +0,0 @@
--
- id: 1004
- uuid: "fb857e63-c0ce-4571-a6c9-fde26c128073"
- name: "Global runner"
- owner_id: 0
- repo_id: 0
- deleted: 0
diff --git a/routers/web/shared/actions/fixtures/TestRunnerDetails/action_task.yml b/routers/web/shared/actions/fixtures/TestRunnerDetails/action_task.yml
deleted file mode 100644
index 63a2d30deb..0000000000
--- a/routers/web/shared/actions/fixtures/TestRunnerDetails/action_task.yml
+++ /dev/null
@@ -1,160 +0,0 @@
--
- id: 1
- runner_id: 1004
- token_hash: a1
--
- id: 2
- runner_id: 1004
- token_hash: a2
--
- id: 3
- runner_id: 1004
- token_hash: a3
--
- id: 4
- runner_id: 1004
- token_hash: a4
--
- id: 5
- runner_id: 1004
- token_hash: a5
--
- id: 6
- runner_id: 1004
- token_hash: a6
--
- id: 7
- runner_id: 1004
- token_hash: a7
--
- id: 8
- runner_id: 1004
- token_hash: a8
--
- id: 9
- runner_id: 1004
- token_hash: a9
--
- id: 10
- runner_id: 1004
- token_hash: a10
--
- id: 11
- runner_id: 1004
- token_hash: a11
--
- id: 12
- runner_id: 1004
- token_hash: a12
--
- id: 13
- runner_id: 1004
- token_hash: a13
--
- id: 14
- runner_id: 1004
- token_hash: a14
--
- id: 15
- runner_id: 1004
- token_hash: a15
--
- id: 16
- runner_id: 1004
- token_hash: a16
--
- id: 17
- runner_id: 1004
- token_hash: a17
--
- id: 18
- runner_id: 1004
- token_hash: a18
--
- id: 19
- runner_id: 1004
- token_hash: a19
--
- id: 20
- runner_id: 1004
- token_hash: a20
--
- id: 21
- runner_id: 1004
- token_hash: a21
--
- id: 22
- runner_id: 1004
- token_hash: a22
--
- id: 23
- runner_id: 1004
- token_hash: a23
--
- id: 24
- runner_id: 1004
- token_hash: a24
--
- id: 25
- runner_id: 1004
- token_hash: a25
--
- id: 26
- runner_id: 1004
- token_hash: a26
--
- id: 27
- runner_id: 1004
- token_hash: a27
--
- id: 28
- runner_id: 1004
- token_hash: a28
--
- id: 29
- runner_id: 1004
- token_hash: a29
--
- id: 30
- runner_id: 1004
- token_hash: a30
--
- id: 31
- runner_id: 1004
- token_hash: a31
--
- id: 32
- runner_id: 1004
- token_hash: a32
--
- id: 33
- runner_id: 1004
- token_hash: a33
--
- id: 34
- runner_id: 1004
- token_hash: a34
--
- id: 35
- runner_id: 1004
- token_hash: a35
--
- id: 36
- runner_id: 1004
- token_hash: a36
--
- id: 37
- runner_id: 1004
- token_hash: a37
--
- id: 38
- runner_id: 1004
- token_hash: a38
--
- id: 39
- runner_id: 1004
- token_hash: a39
--
- id: 40
- runner_id: 1004
- token_hash: a40
diff --git a/routers/web/shared/actions/main_test.go b/routers/web/shared/actions/main_test.go
deleted file mode 100644
index 056f48b98d..0000000000
--- a/routers/web/shared/actions/main_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2025 The Forgejo Authors.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package actions
-
-import (
- "testing"
-
- "forgejo.org/models/unittest"
-
- _ "forgejo.org/models"
- _ "forgejo.org/models/forgefed"
-)
-
-func TestMain(m *testing.M) {
- unittest.MainTest(m)
-}
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index 2ab6b2dadd..98a649d1d8 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -79,6 +79,7 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
Page: page,
PageSize: 30,
},
+ Status: []actions_model.Status{actions_model.StatusUnknown}, // Unknown means all
RunnerID: runner.ID,
}
diff --git a/routers/web/shared/actions/runners_test.go b/routers/web/shared/actions/runners_test.go
deleted file mode 100644
index ad75d34ee6..0000000000
--- a/routers/web/shared/actions/runners_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2025 The Forgejo Authors.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package actions
-
-import (
- "net/http"
- "testing"
-
- actions_model "forgejo.org/models/actions"
- "forgejo.org/models/unittest"
- user_model "forgejo.org/models/user"
- "forgejo.org/services/contexttest"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestRunnerDetails(t *testing.T) {
- defer unittest.OverrideFixtures("routers/web/shared/actions/fixtures/TestRunnerDetails")()
- require.NoError(t, unittest.PrepareTestDatabase())
-
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1004})
-
- t.Run("permission denied", func(t *testing.T) {
- ctx, resp := contexttest.MockContext(t, "/admin/actions/runners")
- RunnerDetails(ctx, 1, runner.ID, user.ID, 0)
- assert.Equal(t, http.StatusNotFound, resp.Code)
- })
-
- t.Run("first page", func(t *testing.T) {
- ctx, resp := contexttest.MockContext(t, "/admin/actions/runners")
- page := 1
- RunnerDetails(ctx, page, runner.ID, 0, 0)
- require.Equal(t, http.StatusOK, resp.Code)
- assert.Len(t, ctx.GetData()["Tasks"], 30)
- })
-
- t.Run("second and last page", func(t *testing.T) {
- ctx, resp := contexttest.MockContext(t, "/admin/actions/runners")
- page := 2
- RunnerDetails(ctx, page, runner.ID, 0, 0)
- require.Equal(t, http.StatusOK, resp.Code)
- assert.Len(t, ctx.GetData()["Tasks"], 10)
- })
-}
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index 538c1e4b71..1d4fb1588d 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -9,6 +9,7 @@ import (
"net/http"
"time"
+ "forgejo.org/models/db"
packages_model "forgejo.org/models/packages"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
@@ -167,12 +168,13 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
PackageID: p.ID,
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
+ Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
if err != nil {
ctx.ServerError("SearchVersions", err)
return
}
- for _, pv := range pvs[pcr.KeepCount:] {
+ for _, pv := range pvs {
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
ctx.ServerError("ShouldBeSkipped", err)
return
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 87feb966cb..c26ff19165 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -22,7 +22,6 @@ import (
"forgejo.org/modules/markup/markdown"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
- "forgejo.org/routers/web/repo"
"forgejo.org/services/context"
)
@@ -39,7 +38,6 @@ func prepareContextForCommonProfile(ctx *context.Context) {
func PrepareContextForProfileBigAvatar(ctx *context.Context) {
prepareContextForCommonProfile(ctx)
- ctx.Data["IsModerationEnabled"] = setting.Moderation.Enabled
ctx.Data["IsBlocked"] = ctx.Doer != nil && user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
@@ -68,7 +66,6 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
UserID: ctx.ContextUser.ID,
- IncludeLimited: ctx.IsSigned,
IncludePrivate: showPrivate,
})
if err != nil {
@@ -105,22 +102,7 @@ func FindUserProfileReadme(ctx *context.Context, doer *user_model.User) (profile
if commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch); err != nil {
log.Error("FindUserProfileReadme failed to GetBranchCommit: %v", err)
} else {
- tree, err := commit.SubTree("")
- if err != nil {
- log.Error("FindUserProfileReadme failed to get SubTree: %v", err)
- } else {
- entries, err := tree.ListEntries()
- if err != nil {
- log.Error("FindUserProfileReadme failed to list entries: %v", err)
- } else {
- _, readmeEntry, err := repo.FindReadmeFileInEntries(ctx, entries, true)
- if err != nil {
- log.Error("FindUserProfileReadme failed to find readme in entries: %v", err)
- } else if readmeEntry != nil {
- profileReadmeBlob = readmeEntry.Blob()
- }
- }
- }
+ profileReadmeBlob, _ = commit.GetBlobByFoldedPath("README.md")
}
}
}
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index b5c5e54953..ac1852e410 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -131,7 +131,6 @@ func CodeSearch(ctx *context.Context) {
pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
pager.SetDefaultParams(ctx)
pager.AddParam(ctx, "l", "Language")
- pager.AddParam(ctx, "mode", "CodeSearchMode")
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplUserCode)
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index d980fa393a..9f22cebaba 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -16,6 +16,7 @@ import (
activities_model "forgejo.org/models/activities"
asymkey_model "forgejo.org/models/asymkey"
"forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
issues_model "forgejo.org/models/issues"
"forgejo.org/models/organization"
repo_model "forgejo.org/models/repo"
@@ -610,6 +611,11 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.ServerError("GetIssuesLastCommitStatus", err)
return
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ for key := range commitStatuses {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
+ }
+ }
// -------------------------------
// Fill stats to post to ctx.Data.
@@ -649,10 +655,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- switch typ {
- case "reject":
+ if typ == "reject" {
reviewTyp = issues_model.ReviewTypeReject
- case "waiting":
+ } else if typ == "waiting" {
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index f3a2f12ae6..af9d50538d 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -40,15 +40,15 @@ func TestArchivedIssues(t *testing.T) {
NumIssues[repo.ID] = repo.NumIssues
}
assert.False(t, IsArchived[50])
- assert.Equal(t, 1, NumIssues[50])
+ assert.EqualValues(t, 1, NumIssues[50])
assert.True(t, IsArchived[51])
- assert.Equal(t, 1, NumIssues[51])
+ assert.EqualValues(t, 1, NumIssues[51])
// Act
Issues(ctx)
// Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.Len(t, ctx.Data["Issues"], 1)
}
@@ -61,7 +61,7 @@ func TestIssues(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "closed")
Issues(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.Len(t, ctx.Data["Issues"], 1)
@@ -76,7 +76,7 @@ func TestPulls(t *testing.T) {
ctx.Req.Form.Set("state", "open")
ctx.Req.Form.Set("type", "your_repositories")
Pulls(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.Len(t, ctx.Data["Issues"], 5)
}
@@ -91,15 +91,15 @@ func TestMilestones(t *testing.T) {
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
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.Equal(t, "user2/glob", ctx.Data["Repos"].(repo_model.RepositoryList)[0].FullName())
- assert.Equal(t, "user2/repo1", ctx.Data["Repos"].(repo_model.RepositoryList)[1].FullName())
+ 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) {
@@ -113,7 +113,7 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
@@ -144,7 +144,7 @@ func TestOrgLabels(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadOrganization(t, ctx, 3)
Issues(ctx)
- assert.Equal(t, http.StatusOK, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
assert.True(t, ctx.Data["PageIsOrgIssues"].(bool))
@@ -163,9 +163,9 @@ func TestOrgLabels(t *testing.T) {
if assert.Len(t, labels, len(orgLabels)) {
for i, label := range labels {
- assert.Equal(t, orgLabels[i].OrgID, label.OrgID)
- assert.Equal(t, orgLabels[i].ID, label.ID)
- assert.Equal(t, orgLabels[i].Name, label.Name)
+ assert.EqualValues(t, orgLabels[i].OrgID, label.OrgID)
+ assert.EqualValues(t, orgLabels[i].ID, label.ID)
+ assert.EqualValues(t, orgLabels[i].Name, label.Name)
}
}
}
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index fdca1a2fdd..296951b2ff 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -13,8 +13,10 @@ import (
activities_model "forgejo.org/models/activities"
"forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
issues_model "forgejo.org/models/issues"
repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
"forgejo.org/modules/base"
"forgejo.org/modules/log"
"forgejo.org/modules/optional"
@@ -309,6 +311,11 @@ func NotificationSubscriptions(ctx *context.Context) {
ctx.ServerError("GetIssuesAllCommitStatus", err)
return
}
+ if !ctx.Repo.CanRead(unit.TypeActions) {
+ for key := range commitStatuses {
+ git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
+ }
+ }
ctx.Data["CommitLastStatus"] = lastStatus
ctx.Data["CommitStatuses"] = commitStatuses
ctx.Data["Issues"] = issues
@@ -333,10 +340,9 @@ func NotificationSubscriptions(ctx *context.Context) {
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- switch typ {
- case "reject":
+ if typ == "reject" {
reviewTyp = issues_model.ReviewTypeReject
- case "waiting":
+ } else if typ == "waiting" {
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 02cea05a47..5132b1da5c 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -1,6 +1,5 @@
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
-// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@@ -8,8 +7,6 @@ package user
import (
"errors"
"fmt"
- gotemplate "html/template"
- "io"
"net/http"
"path"
"strings"
@@ -72,6 +69,17 @@ func userProfile(ctx *context.Context) {
ctx.Data["OpenGraphURL"] = ctx.ContextUser.HTMLURL()
ctx.Data["OpenGraphDescription"] = ctx.ContextUser.Description
+ // prepare heatmap data
+ if setting.Service.EnableUserHeatmap {
+ data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserHeatmapDataByUser", err)
+ return
+ }
+ ctx.Data["HeatmapData"] = data
+ ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
+ }
+
profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer)
defer profileClose()
@@ -162,32 +170,11 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
ctx.Data["Cards"] = followers
total = int(numFollowers)
ctx.Data["CardsTitle"] = ctx.TrN(total, "user.followers.title.one", "user.followers.title.few")
- if ctx.IsSigned && ctx.ContextUser.ID == ctx.Doer.ID {
- ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.incoming.list.self.none")
- } else {
- ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.incoming.list.none")
- }
case "following":
ctx.Data["Cards"] = following
total = int(numFollowing)
ctx.Data["CardsTitle"] = ctx.TrN(total, "user.following.title.one", "user.following.title.few")
- if ctx.IsSigned && ctx.ContextUser.ID == ctx.Doer.ID {
- ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.outgoing.list.self.none")
- } else {
- ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.outgoing.list.none", ctx.ContextUser.Name)
- }
case "activity":
- // prepare heatmap data
- if setting.Service.EnableUserHeatmap {
- data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserHeatmapDataByUser", err)
- return
- }
- ctx.Data["HeatmapData"] = data
- ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
- }
-
date := ctx.FormString("date")
pagingNum = setting.UI.FeedPagingNum
items, count, err := activities_model.GetFeeds(ctx, activities_model.GetFeedsOptions{
@@ -266,38 +253,26 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
total = int(count)
case "overview":
- if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil {
- log.Error("failed to NewTruncatedReader: %v", err)
+ if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
+ log.Error("failed to GetBlobContent: %v", err)
} else {
- defer rc.Close()
-
- if markupType := markup.Type(profileReadme.Name()); markupType != "" {
- if profileContent, err := markdown.RenderReader(&markup.RenderContext{
- Ctx: ctx,
- Type: markupType,
- GitRepo: profileGitRepo,
- Links: markup.Links{
- // Give the repo link to the markdown render for the full link of media element.
- // the media link usually be like /[user]/[repoName]/media/branch/[branchName],
- // Eg. /Tom/.profile/media/branch/main
- // The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
- // https://docs.gitea.com/usage/profile-readme
- Base: profileDbRepo.Link(),
- BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
- },
- Metas: map[string]string{"mode": "document"},
- }, rc); err != nil {
- log.Error("failed to RenderString: %v", err)
- } else {
- ctx.Data["ProfileReadme"] = profileContent
- }
+ if profileContent, err := markdown.RenderString(&markup.RenderContext{
+ Ctx: ctx,
+ GitRepo: profileGitRepo,
+ Links: markup.Links{
+ // Give the repo link to the markdown render for the full link of media element.
+ // the media link usually be like /[user]/[repoName]/media/branch/[branchName],
+ // Eg. /Tom/.profile/media/branch/main
+ // The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
+ // https://docs.gitea.com/usage/profile-readme
+ Base: profileDbRepo.Link(),
+ BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
+ },
+ Metas: map[string]string{"mode": "document"},
+ }, bytes); err != nil {
+ log.Error("failed to RenderString: %v", err)
} else {
- content, err := io.ReadAll(rc)
- if err != nil {
- log.Error("Read readme content failed: %v", err)
- }
- ctx.Data["ProfileReadme"] = gotemplate.HTMLEscapeString(util.UnsafeBytesToString(content))
- ctx.Data["IsProfileReadmePlain"] = true
+ ctx.Data["ProfileReadme"] = profileContent
}
}
default: // default to "repositories"
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 1dfcc90e35..a0cdb25f44 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -57,7 +57,7 @@ func AccountPost(ctx *context.Context) {
return
}
- if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(ctx, form.OldPassword) {
+ if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
ctx.Flash.Error(ctx.Tr("settings.password_incorrect"))
} else if form.Password != form.Retype {
ctx.Flash.Error(ctx.Tr("form.password_not_match"))
@@ -178,10 +178,10 @@ func EmailPost(ctx *context.Context) {
// Set Email Notification Preference
if ctx.FormString("_method") == "NOTIFICATION" {
preference := ctx.FormString("preference")
- if preference != user_model.EmailNotificationsEnabled &&
- preference != user_model.EmailNotificationsOnMention &&
- preference != user_model.EmailNotificationsDisabled &&
- preference != user_model.EmailNotificationsAndYourOwn {
+ if !(preference == user_model.EmailNotificationsEnabled ||
+ preference == user_model.EmailNotificationsOnMention ||
+ preference == user_model.EmailNotificationsDisabled ||
+ preference == user_model.EmailNotificationsAndYourOwn) {
log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name)
ctx.ServerError("SetEmailPreference", errors.New("option unrecognized"))
return
@@ -212,7 +212,7 @@ func EmailPost(ctx *context.Context) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
- } else if validation.IsErrEmailInvalid(err) {
+ } else if validation.IsErrEmailCharIsNotSupported(err) || validation.IsErrEmailInvalid(err) {
loadAccountData(ctx)
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
index 3f7e1c13bc..82e00bbf7c 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) {
AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
- assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
})
}
}
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
index 59ff31162b..f7fd1c3803 100644
--- a/routers/web/user/setting/adopt.go
+++ b/routers/web/user/setting/adopt.go
@@ -16,8 +16,12 @@ import (
// AdoptOrDeleteRepository adopts or deletes a repository
func AdoptOrDeleteRepository(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("settings.adopt")
+ ctx.Data["PageIsSettingsRepos"] = true
allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
+ ctx.Data["allowAdopt"] = allowAdopt
allowDelete := ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
+ ctx.Data["allowDelete"] = allowDelete
dir := ctx.FormString("id")
action := ctx.FormString("action")
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index e73239b79b..631d5958ea 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -49,9 +49,6 @@ func ApplicationsPost(ctx *context.Context) {
ctx.ServerError("GetScope", err)
return
}
- if !scope.HasPermissionScope() {
- ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true)
- }
t := &auth_model.AccessToken{
UID: ctx.Doer.ID,
Name: form.Name,
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 935efd7ba7..94d32b730f 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -5,7 +5,7 @@
package setting
import (
- "errors"
+ "fmt"
"net/http"
asymkey_model "forgejo.org/models/asymkey"
@@ -80,7 +80,7 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "gpg":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
- ctx.NotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
return
}
@@ -161,7 +161,7 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
- ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
return
}
@@ -205,7 +205,7 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
- ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
return
}
@@ -242,7 +242,7 @@ func DeleteKey(ctx *context.Context) {
switch ctx.FormString("type") {
case "gpg":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
- ctx.NotFound("Not Found", errors.New("gpg keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
return
}
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
@@ -252,7 +252,7 @@ func DeleteKey(ctx *context.Context) {
}
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
- ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
return
}
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index e0ce88b582..173550ad19 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -51,9 +51,6 @@ func Profile(ctx *context.Context) {
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod
ctx.Data["CommonPronouns"] = commonPronouns
- ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize
- ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth
- ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight
ctx.HTML(http.StatusOK, tplSettingsProfile)
}
@@ -66,9 +63,6 @@ func ProfilePost(ctx *context.Context) {
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
ctx.Data["CooldownPeriod"] = setting.Service.UsernameCooldownPeriod
ctx.Data["CommonPronouns"] = commonPronouns
- ctx.Data["MaxAvatarFileSize"] = setting.Avatar.MaxFileSize
- ctx.Data["MaxAvatarWidth"] = setting.Avatar.MaxWidth
- ctx.Data["MaxAvatarHeight"] = setting.Avatar.MaxHeight
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSettingsProfile)
@@ -151,8 +145,8 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
return fmt.Errorf("io.ReadAll: %w", err)
}
- st := typesniffer.DetectContentType(data, "")
- if !st.IsImage() || st.IsSvgImage() {
+ st := typesniffer.DetectContentType(data)
+ if !(st.IsImage() && !st.IsSvgImage()) {
return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
}
if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil {
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index d23917f8b2..f1271c8370 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -40,7 +40,11 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
return
}
- token := t.GenerateScratchToken()
+ token, err := t.GenerateScratchToken()
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
+ return
+ }
if err = auth.UpdateTwoFactor(ctx, t); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
@@ -56,21 +60,6 @@ func DisableTwoFactor(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
- if ctx.Doer.MustHaveTwoFactor() {
- ctx.NotFound("DisableTwoFactor", nil)
- return
- }
-
- disableTwoFactor(ctx)
- if ctx.Written() {
- return
- }
-
- ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
- ctx.Redirect(setting.AppSubURL + "/user/settings/security")
-}
-
-func disableTwoFactor(ctx *context.Context) {
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
if auth.IsErrTwoFactorNotEnrolled(err) {
@@ -97,6 +86,9 @@ func disableTwoFactor(ctx *context.Context) {
ctx.ServerError("SendDisabledTOTP", err)
return
}
+
+ ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
+ ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
func twofaGenerateSecretAndQr(ctx *context.Context) bool {
@@ -184,6 +176,7 @@ func EnrollTwoFactor(ctx *context.Context) {
// EnrollTwoFactorPost handles enrolling the user into 2FA.
func EnrollTwoFactorPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
@@ -199,12 +192,6 @@ func EnrollTwoFactorPost(ctx *context.Context) {
return
}
- enrollTwoFactor(ctx)
-}
-
-func enrollTwoFactor(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
-
if ctx.HasError() {
if !twofaGenerateSecretAndQr(ctx) {
return
@@ -230,10 +217,14 @@ func enrollTwoFactor(ctx *context.Context) {
return
}
- twoFactor := &auth.TwoFactor{
+ t = &auth.TwoFactor{
UID: ctx.Doer.ID,
}
- token := twoFactor.GenerateScratchToken()
+ token, err := t.GenerateScratchToken()
+ if err != nil {
+ ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
+ return
+ }
// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
@@ -255,7 +246,7 @@ func enrollTwoFactor(ctx *context.Context) {
return
}
- if err := auth.NewTwoFactor(ctx, twoFactor, secret); err != nil {
+ if err = auth.NewTwoFactor(ctx, t, secret); err != nil {
// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
// If there is a unique constraint fail we should just tolerate the error
ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
@@ -265,41 +256,3 @@ func enrollTwoFactor(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
-
-// ReenrollTwoFactor shows the page where the user can reenroll 2FA.
-func ReenrollTwoFactor(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsSecurity"] = true
- ctx.Data["ReenrollTwofa"] = true
-
- _, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
- if auth.IsErrTwoFactorNotEnrolled(err) {
- ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled"))
- ctx.Redirect(setting.AppSubURL + "/user/settings/security")
- return
- }
- if err != nil {
- ctx.ServerError("SettingsTwoFactor: GetTwoFactorByUID", err)
- return
- }
-
- if !twofaGenerateSecretAndQr(ctx) {
- return
- }
-
- ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
-}
-
-// ReenrollTwoFactorPost handles reenrolling the user 2FA.
-func ReenrollTwoFactorPost(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("settings")
- ctx.Data["PageIsSettingsSecurity"] = true
- ctx.Data["ReenrollTwofa"] = true
-
- disableTwoFactor(ctx)
- if ctx.Written() {
- return
- }
-
- enrollTwoFactor(ctx)
-}
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index 8b801cfebd..9acc6ab6f0 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -55,7 +55,7 @@ func DeleteAccountLink(ctx *context.Context) {
}
func loadSecurityData(ctx *context.Context) {
- enrolled, err := auth_model.HasTOTPByUID(ctx, ctx.Doer.ID)
+ enrolled, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index 4059139552..296c44f809 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -35,7 +35,7 @@ func TaskStatus(ctx *context.Context) {
var translatableMessage admin_model.TranslatableMessage
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
translatableMessage = admin_model.TranslatableMessage{
- Format: "repo.migrate.migrating_failed.error",
+ Format: "migrate.migrating_failed.error",
Args: []any{task.Message},
}
}
diff --git a/routers/web/web.go b/routers/web/web.go
index 20d5376cfe..303167a6b9 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1,16 +1,13 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
-// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
gocontext "context"
- "fmt"
"net/http"
"strings"
- auth_model "forgejo.org/models/auth"
"forgejo.org/models/perm"
quota_model "forgejo.org/models/quota"
"forgejo.org/models/unit"
@@ -34,7 +31,6 @@ import (
"forgejo.org/routers/web/feed"
"forgejo.org/routers/web/healthcheck"
"forgejo.org/routers/web/misc"
- "forgejo.org/routers/web/moderation"
"forgejo.org/routers/web/org"
org_setting "forgejo.org/routers/web/org/setting"
"forgejo.org/routers/web/repo"
@@ -119,7 +115,7 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
return func(ctx *context.Context) {
ar, err := common.AuthShared(ctx.Base, ctx.Session, authMethod)
if err != nil {
- log.Info("Failed to verify user: %v", err)
+ log.Error("Failed to verify user: %v", err)
ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.unauthorized_credentials", "https://codeberg.org/forgejo/forgejo/issues/2809"))
return
}
@@ -171,19 +167,6 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
ctx.Redirect(setting.AppSubURL + "/")
return
}
-
- if ctx.Doer.MustHaveTwoFactor() && !strings.HasPrefix(ctx.Req.URL.Path, "/user/settings/security") {
- hasTwoFactor, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer.ID)
- if err != nil {
- log.Error("Error getting 2fa: %s", err)
- ctx.Error(http.StatusInternalServerError, "HasTwoFactorByUID", err.Error())
- return
- }
- if !hasTwoFactor {
- ctx.Redirect(setting.AppSubURL + "/user/settings/security")
- return
- }
- }
}
// Redirect to dashboard (or alternate location) if user tries to visit any non-login page.
@@ -192,8 +175,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
return
}
- safeMethod := ctx.Req.Method == "GET" || ctx.Req.Method == "HEAD" || ctx.Req.Method == "OPTIONS"
- if !options.SignOutRequired && !options.DisableCSRF && !safeMethod {
+ if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" {
ctx.Csrf.Validate(ctx)
if ctx.Written() {
return
@@ -325,20 +307,6 @@ func registerRoutes(m *web.Route) {
}
}
- requiredTwoFactor := func(ctx *context.Context) {
- if !ctx.Doer.MustHaveTwoFactor() {
- return
- }
-
- hasTwoFactor, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, fmt.Sprintf("Error getting 2fa: %s", err))
- return
- }
- ctx.Data["MustEnableTwoFactor"] = !hasTwoFactor
- ctx.Data["HideNavbarLinks"] = !hasTwoFactor
- }
-
openIDSignInEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignIn {
ctx.Error(http.StatusForbidden)
@@ -505,11 +473,6 @@ func registerRoutes(m *web.Route) {
m.Get("/search", repo.SearchIssues)
}, reqSignIn)
- if setting.Moderation.Enabled {
- m.Get("/report_abuse", reqSignIn, moderation.NewReport)
- m.Post("/report_abuse", reqSignIn, web.Bind(forms.ReportAbuseForm{}), moderation.CreatePost)
- }
-
m.Get("/pulls", reqSignIn, user.Pulls)
m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
@@ -594,8 +557,6 @@ func registerRoutes(m *web.Route) {
m.Post("/disable", security.DisableTwoFactor)
m.Get("/enroll", security.EnrollTwoFactor)
m.Post("/enroll", web.Bind(forms.TwoFactorAuthForm{}), security.EnrollTwoFactorPost)
- m.Get("/reenroll", security.ReenrollTwoFactor)
- m.Post("/reenroll", web.Bind(forms.TwoFactorAuthForm{}), security.ReenrollTwoFactorPost)
})
m.Group("/webauthn", func() {
m.Post("/request_register", web.Bind(forms.WebauthnRegistrationForm{}), security.WebAuthnRegister)
@@ -608,7 +569,7 @@ func registerRoutes(m *web.Route) {
m.Post("/toggle_visibility", security.ToggleOpenIDVisibility)
}, openIDSignInEnabled)
m.Post("/account_link", linkAccountEnabled, security.DeleteAccountLink)
- }, requiredTwoFactor)
+ })
m.Group("/applications", func() {
// oauth2 applications
@@ -813,14 +774,7 @@ func registerRoutes(m *web.Route) {
addSettingsRunnersRoutes()
addSettingsVariablesRoutes()
})
-
- if setting.Moderation.Enabled {
- m.Group("/moderation/reports", func() {
- m.Get("", admin.AbuseReports)
- m.Get("/type/{type:1|2|3|4}/id/{id}", admin.AbuseReportDetails)
- })
- }
- }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "EnableModeration", setting.Moderation.Enabled))
+ }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled))
// ***** END: Admin *****
m.Group("", func() {
@@ -857,13 +811,13 @@ func registerRoutes(m *web.Route) {
individualPermsChecker := func(ctx *context.Context) {
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
if ctx.ContextUser.IsIndividual() {
- switch ctx.ContextUser.Visibility {
- case structs.VisibleTypePrivate:
+ switch {
+ case ctx.ContextUser.Visibility == structs.VisibleTypePrivate:
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
ctx.NotFound("Visit Project", nil)
return
}
- case structs.VisibleTypeLimited:
+ case ctx.ContextUser.Visibility == structs.VisibleTypeLimited:
if ctx.Doer == nil {
ctx.NotFound("Visit Project", nil)
return
@@ -1438,28 +1392,25 @@ func registerRoutes(m *web.Route) {
m.Get("", actions.List)
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
- m.Post("/manual", reqRepoActionsWriter, actions.ManualRunWorkflow)
+ m.Post("/manual", reqRepoAdmin, actions.ManualRunWorkflow)
m.Group("/runs", func() {
m.Get("/latest", actions.ViewLatest)
m.Group("/{run}", func() {
m.Combo("").
- Get(actions.RedirectToLatestAttempt).
+ Get(actions.View).
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
m.Group("/jobs/{job}", func() {
m.Combo("").
- Get(actions.RedirectToLatestAttempt).
+ Get(actions.View).
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
m.Get("/logs", actions.Logs)
- m.Combo("/attempt/{attempt}").
- Get(actions.View).
- Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
})
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
m.Get("/artifacts", actions.ArtifactsView)
- m.Get("/artifacts/{artifact_name_or_id}", actions.ArtifactsDownloadView)
+ m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
})
@@ -1503,7 +1454,7 @@ func registerRoutes(m *web.Route) {
}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
m.Group("/recent-commits", func() {
m.Get("", repo.RecentCommits)
- m.Get("/data", repo.CodeFrequencyData)
+ m.Get("/data", repo.RecentCommitsData)
}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
}, context.RepoRef(), context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
@@ -1552,10 +1503,7 @@ func registerRoutes(m *web.Route) {
m.Group("/commits", func() {
m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
m.Get("/list", context.RepoRef(), repo.GetPullCommits)
- m.Group("/{sha:[a-f0-9]{4,40}}", func() {
- m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
- m.Post("/reviews/submit", context.RepoMustNotBeArchived(), web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview)
- })
+ m.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
})
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), context.EnforceQuotaWeb(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
@@ -1643,8 +1591,6 @@ func registerRoutes(m *web.Route) {
}, context.RepoRef(), reqRepoCodeReader)
}
m.Get("/commit/{sha:([a-f0-9]{4,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, reqRepoCodeReader, repo.RawDiff)
-
- m.Post("/sync_fork", context.RepoMustNotBeArchived(), repo.MustBeNotEmpty, reqRepoCodeWriter, repo.SyncFork)
}, ignSignIn, context.RepoAssignment, context.UnitTypes())
m.Post("/{username}/{reponame}/lastcommit/*", ignSignInAndCsrf, context.RepoAssignment, context.UnitTypes(), context.RepoRefByType(context.RepoRefCommit), reqRepoCodeReader, repo.LastCommit)
@@ -1715,7 +1661,6 @@ func registerRoutes(m *web.Route) {
m.Any("/devtest", devtest.List)
m.Any("/devtest/fetch-action-test", devtest.FetchActionTest)
m.Any("/devtest/{sub}", devtest.Tmpl)
- m.Get("/devtest/error/{errcode}", devtest.ErrorPage)
}
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go
index 5f67e436bf..be3c2925fe 100644
--- a/routers/web/webfinger.go
+++ b/routers/web/webfinger.go
@@ -58,33 +58,7 @@ func WebfingerQuery(ctx *context.Context) {
return
}
- // Instance actor
- if parts[0] == "ghost" {
- aliases := []string{
- appURL.String() + "api/v1/activitypub/actor",
- }
-
- links := []*webfingerLink{
- {
- Rel: "self",
- Type: "application/activity+json",
- Href: appURL.String() + "api/v1/activitypub/actor",
- },
- }
-
- ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*")
- ctx.JSON(http.StatusOK, &webfingerJRD{
- Subject: fmt.Sprintf("acct:%s@%s", "ghost", appURL.Host),
- Aliases: aliases,
- Links: links,
- })
- ctx.Resp.Header().Set("Content-Type", "application/jrd+json")
-
- return
- }
-
u, err = user_model.GetUserByName(ctx, parts[0])
-
case "mailto":
u, err = user_model.GetUserByEmail(ctx, resource.Opaque)
if u != nil && u.KeepEmailPrivate {
@@ -179,7 +153,7 @@ func WebfingerQuery(ctx *context.Context) {
},
{
Rel: "http://openid.net/specs/connect/1.0/issuer",
- Href: strings.TrimSuffix(appURL.String(), "/"),
+ Href: appURL.String(),
},
}
diff --git a/services/actions/TestServiceActions_startTask/action_schedule.yml b/services/actions/TestServiceActions_startTask/action_schedule.yml
deleted file mode 100644
index d0e7234475..0000000000
--- a/services/actions/TestServiceActions_startTask/action_schedule.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-# A corrupted cron spec with a valid schedule workflow
--
- id: 1
- title: schedule_title1
- specs:
- - '* * * * *'
- repo_id: 4
- owner_id: 2
- workflow_id: 'workflow1.yml'
- trigger_user_id: 2
- ref: main
- commit_sha: shashasha
- event: "schedule"
- event_payload: "fakepayload"
- content: |
- jobs:
- job2:
- runs-on: ubuntu-latest
- steps:
- - run: true
-
-# A valid cron spec with a corrupted schedule workflow
--
- id: 2
- title: schedule_title2
- specs:
- - '* * * * *'
- repo_id: 4
- owner_id: 2
- workflow_id: 'workflow2.yml'
- trigger_user_id: 2
- ref: main
- commit_sha: shashasha
- event: "schedule"
- event_payload: "fakepayload"
- content: |
- jobs:
- job2: { invalid yaml
- runs-on: ubuntu-latest
- steps:
- - run: true
diff --git a/services/actions/TestServiceActions_startTask/action_schedule_spec.yml b/services/actions/TestServiceActions_startTask/action_schedule_spec.yml
deleted file mode 100644
index 7bcc78f010..0000000000
--- a/services/actions/TestServiceActions_startTask/action_schedule_spec.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# A corrupted cron spec with a valid schedule workflow
--
- id: 1
- repo_id: 4
- schedule_id: 1
- next: 1
- spec: 'corrupted * *'
-
-# A valid cron spec with a corrupted schedule workflow
--
- id: 2
- repo_id: 4
- schedule_id: 2
- next: 1
- spec: '* * * * *'
diff --git a/services/actions/auth.go b/services/actions/auth.go
index 98b618aeba..4dc86a35f3 100644
--- a/services/actions/auth.go
+++ b/services/actions/auth.go
@@ -4,7 +4,6 @@
package actions
import (
- "errors"
"fmt"
"net/http"
"strings"
@@ -81,7 +80,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 {
log.Error("split token failed: %s", h)
- return 0, errors.New("split token failed")
+ return 0, fmt.Errorf("split token failed")
}
return TokenToTaskID(parts[1])
@@ -101,7 +100,7 @@ func TokenToTaskID(token string) (int64, error) {
c, ok := parsedToken.Claims.(*actionsClaims)
if !parsedToken.Valid || !ok {
- return 0, errors.New("invalid token claim")
+ return 0, fmt.Errorf("invalid token claim")
}
return c.TaskID, nil
diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go
index d9f0437e1b..93a5980bc5 100644
--- a/services/actions/auth_test.go
+++ b/services/actions/auth_test.go
@@ -19,7 +19,7 @@ func TestCreateAuthorizationToken(t *testing.T) {
var taskID int64 = 23
token, err := CreateAuthorizationToken(taskID, 1, 2)
require.NoError(t, err)
- assert.NotEmpty(t, token)
+ assert.NotEqual(t, "", token)
claims := jwt.MapClaims{}
_, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
return setting.GetGeneralTokenSigningSecret(), nil
@@ -45,7 +45,7 @@ func TestParseAuthorizationToken(t *testing.T) {
var taskID int64 = 23
token, err := CreateAuthorizationToken(taskID, 1, 2)
require.NoError(t, err)
- assert.NotEmpty(t, token)
+ assert.NotEqual(t, "", token)
headers := http.Header{}
headers.Set("Authorization", "Bearer "+token)
rTaskID, err := ParseAuthorizationToken(&http.Request{
diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go
index 918be0f185..fde5286e60 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -126,9 +126,3 @@ func CleanupLogs(ctx context.Context) error {
log.Info("Removed %d logs", count)
return nil
}
-
-// CleanupOfflineRunners removes offline runners
-func CleanupOfflineRunners(ctx context.Context, duration time.Duration, globalOnly bool) error {
- olderThan := timeutil.TimeStampNow().AddDuration(-duration)
- return actions_model.DeleteOfflineRunners(ctx, olderThan, globalOnly)
-}
diff --git a/services/actions/cleanup_test.go b/services/actions/cleanup_test.go
index 4a847ced23..67f68d4de9 100644
--- a/services/actions/cleanup_test.go
+++ b/services/actions/cleanup_test.go
@@ -24,7 +24,7 @@ func TestCleanup(t *testing.T) {
require.NoError(t, CleanupLogs(db.DefaultContext))
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 1001})
- assert.Equal(t, "does-not-exist", task.LogFilename)
+ assert.EqualValues(t, "does-not-exist", task.LogFilename)
assert.True(t, task.LogExpired)
assert.Nil(t, task.LogIndexes)
})
diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go
index c36dda55b2..31e15ec927 100644
--- a/services/actions/clear_tasks.go
+++ b/services/actions/clear_tasks.go
@@ -41,7 +41,7 @@ func stopTasks(ctx context.Context, opts actions_model.FindTaskOptions) error {
jobs := make([]*actions_model.ActionRunJob, 0, len(tasks))
for _, task := range tasks {
if err := db.WithTx(ctx, func(ctx context.Context) error {
- if err := StopTask(ctx, task.ID, actions_model.StatusFailure); err != nil {
+ if err := actions_model.StopTask(ctx, task.ID, actions_model.StatusFailure); err != nil {
return err
}
if err := task.LoadJob(ctx); err != nil {
@@ -88,7 +88,7 @@ func CancelAbandonedJobs(ctx context.Context) error {
job.Status = actions_model.StatusCancelled
job.Stopped = now
if err := db.WithTx(ctx, func(ctx context.Context) error {
- _, err := UpdateRunJob(ctx, job, nil, "status", "stopped")
+ _, err := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped")
return err
}); err != nil {
log.Warn("cancel abandoned job %v: %v", job.ID, err)
diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go
index d45b75ac0b..1fffa6852f 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -5,7 +5,6 @@ package actions
import (
"context"
- "errors"
"fmt"
"path"
@@ -19,7 +18,7 @@ import (
webhook_module "forgejo.org/modules/webhook"
commitstatus_service "forgejo.org/services/repository/commitstatus"
- "code.forgejo.org/forgejo/runner/v11/act/jobparser"
+ "github.com/nektos/act/pkg/jobparser"
)
// CreateCommitStatus creates a commit status for the given job.
@@ -51,7 +50,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
return fmt.Errorf("GetPushEventPayload: %w", err)
}
if payload.HeadCommit == nil {
- return errors.New("head commit is missing in event payload")
+ return fmt.Errorf("head commit is missing in event payload")
}
sha = payload.HeadCommit.ID
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestMilestone:
@@ -65,9 +64,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
return fmt.Errorf("GetPullRequestEventPayload: %w", err)
}
if payload.PullRequest == nil {
- return errors.New("pull request is missing in event payload")
+ return fmt.Errorf("pull request is missing in event payload")
} else if payload.PullRequest.Head == nil {
- return errors.New("head of pull request is missing in event payload")
+ return fmt.Errorf("head of pull request is missing in event payload")
}
sha = payload.PullRequest.Head.Sha
case webhook_module.HookEventRelease:
@@ -80,7 +79,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
repo := run.Repo
// TODO: store workflow name as a field in ActionRun to avoid parsing
runName := path.Base(run.WorkflowID)
- if wfs, err := jobparser.Parse(job.WorkflowPayload, false); err == nil && len(wfs) > 0 {
+ if wfs, err := jobparser.Parse(job.WorkflowPayload); err == nil && len(wfs) > 0 {
runName = wfs[0].Name
}
ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index 8e58105e47..d4ca029d46 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -13,7 +13,7 @@ import (
"forgejo.org/modules/graceful"
"forgejo.org/modules/queue"
- "code.forgejo.org/forgejo/runner/v11/act/jobparser"
+ "github.com/nektos/act/pkg/jobparser"
"xorm.io/builder"
)
@@ -59,7 +59,7 @@ func checkJobsOfRun(ctx context.Context, runID int64) error {
for _, job := range jobs {
if status, ok := updates[job.ID]; ok {
job.Status = status
- if n, err := UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil {
+ if n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil {
return err
} else if n != 1 {
return fmt.Errorf("no affected for updating blocked job %v", job.ID)
@@ -142,7 +142,7 @@ func (r *jobStatusResolver) resolve() map[int64]actions_model.Status {
} else {
// Check if the job has an "if" condition
hasIf := false
- if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload, false); len(wfJobs) == 1 {
+ if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload); len(wfJobs) == 1 {
_, wfJob := wfJobs[0].Job()
hasIf = len(wfJob.If.Value) > 0
}
diff --git a/services/actions/job_parser.go b/services/actions/job_parser.go
deleted file mode 100644
index 8c880977d1..0000000000
--- a/services/actions/job_parser.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package actions
-
-import (
- "fmt"
-
- "code.forgejo.org/forgejo/runner/v11/act/jobparser"
-)
-
-func jobParser(workflow []byte, options ...jobparser.ParseOption) ([]*jobparser.SingleWorkflow, error) {
- singleWorkflows, err := jobparser.Parse(workflow, false, options...)
- if err != nil {
- return nil, err
- }
- nameToSingleWorkflows := make(map[string][]*jobparser.SingleWorkflow, len(singleWorkflows))
- duplicates := make(map[string]int, len(singleWorkflows))
- for _, singleWorkflow := range singleWorkflows {
- id, job := singleWorkflow.Job()
- nameToSingleWorkflows[job.Name] = append(nameToSingleWorkflows[job.Name], singleWorkflow)
- if len(nameToSingleWorkflows[job.Name]) > 1 {
- duplicates[job.Name]++
- job.Name = fmt.Sprintf("%s-%d", job.Name, duplicates[job.Name])
- if err := singleWorkflow.SetJob(id, job); err != nil {
- return nil, err
- }
- }
- }
- return singleWorkflows, nil
-}
diff --git a/services/actions/job_parser_test.go b/services/actions/job_parser_test.go
deleted file mode 100644
index 9c1361d74e..0000000000
--- a/services/actions/job_parser_test.go
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package actions
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestServiceActions_jobParser(t *testing.T) {
- for _, testCase := range []struct {
- name string
- workflow string
- singleWorkflows []string
- }{
- {
- name: "OneJobNoDuplicate",
- workflow: `
-jobs:
- job1:
- runs-on: docker
- steps:
- - run: echo OK
-`,
- singleWorkflows: []string{
- `jobs:
- job1:
- name: job1
- runs-on: docker
- steps:
- - run: echo OK
-`,
- },
- },
- {
- name: "MatrixTwoJobsWithSameJobName",
- workflow: `
-name: test
-jobs:
- job1:
- name: shadowdefaultmatrixgeneratednames
- strategy:
- matrix:
- version: [1.17, 1.19]
- runs-on: docker
- steps:
- - run: echo OK
-`,
- singleWorkflows: []string{
- `name: test
-jobs:
- job1:
- name: shadowdefaultmatrixgeneratednames
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.17
-`,
- `name: test
-jobs:
- job1:
- name: shadowdefaultmatrixgeneratednames-1
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.19
-`,
- },
- },
- {
- name: "MatrixTwoJobsWithMatrixGeneratedNames",
- workflow: `
-name: test
-jobs:
- job1:
- strategy:
- matrix:
- version: [1.17, 1.19]
- runs-on: docker
- steps:
- - run: echo OK
-`,
- singleWorkflows: []string{
- `name: test
-jobs:
- job1:
- name: job1 (1.17)
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.17
-`,
- `name: test
-jobs:
- job1:
- name: job1 (1.19)
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.19
-`,
- },
- },
- {
- name: "MatrixTwoJobsWithDistinctInterpolatedNames",
- workflow: `
-name: test
-jobs:
- job1:
- name: myname-${{ matrix.version }}
- strategy:
- matrix:
- version: [1.17, 1.19]
- runs-on: docker
- steps:
- - run: echo OK
-`,
- singleWorkflows: []string{
- `name: test
-jobs:
- job1:
- name: myname-1.17
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.17
-`,
- `name: test
-jobs:
- job1:
- name: myname-1.19
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.19
-`,
- },
- },
- {
- name: "MatrixTwoJobsWithIdenticalInterpolatedNames",
- workflow: `
-name: test
-jobs:
- job1:
- name: myname-${{ matrix.typo }}
- strategy:
- matrix:
- version: [1.17, 1.19]
- runs-on: docker
- steps:
- - run: echo OK
-`,
- singleWorkflows: []string{
- `name: test
-jobs:
- job1:
- name: myname-
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.17
-`,
- `name: test
-jobs:
- job1:
- name: myname--1
- runs-on: docker
- steps:
- - run: echo OK
- strategy:
- matrix:
- version:
- - 1.19
-`,
- },
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- sw, err := jobParser([]byte(testCase.workflow))
- require.NoError(t, err)
- for i, sw := range sw {
- actual, err := sw.Marshal()
- require.NoError(t, err)
- assert.Equal(t, testCase.singleWorkflows[i], string(actual))
- }
- })
- }
-}
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 7b291d491b..2d3a1d2107 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -5,9 +5,7 @@ package actions
import (
"context"
- "errors"
- actions_model "forgejo.org/models/actions"
issues_model "forgejo.org/models/issues"
packages_model "forgejo.org/models/packages"
perm_model "forgejo.org/models/perm"
@@ -19,12 +17,9 @@ import (
"forgejo.org/modules/repository"
"forgejo.org/modules/setting"
api "forgejo.org/modules/structs"
- "forgejo.org/modules/util"
webhook_module "forgejo.org/modules/webhook"
"forgejo.org/services/convert"
notify_service "forgejo.org/services/notify"
-
- "xorm.io/builder"
)
type actionsNotifier struct {
@@ -227,7 +222,7 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
var apiLabel *api.Label
if action == api.HookIssueLabelUpdated || action == api.HookIssueLabelCleared {
- apiLabel = convert.ToLabel(label, issue.Repo, issue.Repo.Owner)
+ apiLabel = convert.ToLabel(label, issue.Repo, nil)
}
if issue.IsPull {
@@ -343,7 +338,7 @@ func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, commen
newNotifyInputFromIssue(comment.Issue, event).
WithDoer(doer).
WithPayload(payload).
- WithPullRequestData(comment.Issue.PullRequest).
+ WithPullRequest(comment.Issue.PullRequest).
Notify(ctx)
return
}
@@ -780,76 +775,3 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m
Sender: convert.ToUser(ctx, doer, nil),
}).Notify(ctx)
}
-
-// Call this sendActionRunNowDoneNotificationIfNeeded when there has been an update for an ActionRun.
-// priorRun and updatedRun represent the very same ActionRun, just at different times:
-// priorRun before the update and updatedRun after.
-// The parameter lastRun in the ActionRunNowDone notification represents an entirely different ActionRun:
-// the ActionRun of the same workflow that finished before priorRun/updatedRun.
-func sendActionRunNowDoneNotificationIfNeeded(ctx context.Context, priorRun, updatedRun *actions_model.ActionRun) error {
- if !priorRun.Status.IsDone() && updatedRun.Status.IsDone() {
- lastRun, err := actions_model.GetRunBefore(ctx, updatedRun)
- if err != nil && !errors.Is(err, util.ErrNotExist) {
- return err
- }
- // when no last run was found lastRun is nil
- if lastRun != nil {
- if err = lastRun.LoadAttributes(ctx); err != nil {
- return err
- }
- }
- if err = updatedRun.LoadAttributes(ctx); err != nil {
- return err
- }
- notify_service.ActionRunNowDone(ctx, updatedRun, priorRun.Status, lastRun)
- }
- return nil
-}
-
-// wrapper of UpdateRunWithoutNotification with a call to the ActionRunNowDone notification channel
-func UpdateRun(ctx context.Context, run *actions_model.ActionRun, cols ...string) error {
- // run.ID is the only thing that must be given
- priorRun, err := actions_model.GetRunByID(ctx, run.ID)
- if err != nil {
- return err
- }
-
- if err = actions_model.UpdateRunWithoutNotification(ctx, run, cols...); err != nil {
- return err
- }
-
- updatedRun, err := actions_model.GetRunByID(ctx, run.ID)
- if err != nil {
- return err
- }
- return sendActionRunNowDoneNotificationIfNeeded(ctx, priorRun, updatedRun)
-}
-
-// wrapper of UpdateRunJobWithoutNotification with a call to the ActionRunNowDone notification channel
-func UpdateRunJob(ctx context.Context, job *actions_model.ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
- runID := job.RunID
- if runID == 0 {
- // job.ID is the only thing that must be given
- // Don't overwrite job here, we'd loose the change we need to make.
- oldJob, err := actions_model.GetRunJobByID(ctx, job.ID)
- if err != nil {
- return 0, err
- }
- runID = oldJob.RunID
- }
- priorRun, err := actions_model.GetRunByID(ctx, runID)
- if err != nil {
- return 0, err
- }
-
- affected, err := actions_model.UpdateRunJobWithoutNotification(ctx, job, cond, cols...)
- if err != nil {
- return affected, err
- }
-
- updatedRun, err := actions_model.GetRunByID(ctx, runID)
- if err != nil {
- return affected, err
- }
- return affected, sendActionRunNowDoneNotificationIfNeeded(ctx, priorRun, updatedRun)
-}
diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go
index a9978c32cb..9de0b75ac7 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -30,8 +30,8 @@ import (
webhook_module "forgejo.org/modules/webhook"
"forgejo.org/services/convert"
- "code.forgejo.org/forgejo/runner/v11/act/jobparser"
- "code.forgejo.org/forgejo/runner/v11/act/model"
+ "github.com/nektos/act/pkg/jobparser"
+ "github.com/nektos/act/pkg/model"
)
type methodCtx struct{}
@@ -102,12 +102,6 @@ func (input *notifyInput) WithPayload(payload api.Payloader) *notifyInput {
return input
}
-// for cases like issue comments on PRs, which have the PR data, but don't run on its ref
-func (input *notifyInput) WithPullRequestData(pr *issues_model.PullRequest) *notifyInput {
- input.PullRequest = pr
- return input
-}
-
func (input *notifyInput) WithPullRequest(pr *issues_model.PullRequest) *notifyInput {
input.PullRequest = pr
if input.Ref == "" {
@@ -145,7 +139,7 @@ func notify(ctx context.Context, input *notifyInput) error {
return nil
}
if unit_model.TypeActions.UnitGlobalDisabled() {
- if err := CleanRepoScheduleTasks(ctx, input.Repo, true); err != nil {
+ if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo, true); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
return nil
@@ -225,7 +219,7 @@ func notify(ctx context.Context, input *notifyInput) error {
}
}
- if input.PullRequest != nil && !actions_module.IsDefaultBranchWorkflow(input.Event) {
+ if input.PullRequest != nil {
// detect pull_request_target workflows
baseRef := git.BranchPrefix + input.PullRequest.BaseBranch
baseCommit, err := gitRepo.GetCommit(baseRef)
@@ -321,7 +315,7 @@ func handleWorkflows(
}
isForkPullRequest := false
- if pr := input.PullRequest; pr != nil && !actions_module.IsDefaultBranchWorkflow(input.Event) {
+ if pr := input.PullRequest; pr != nil {
switch pr.Flow {
case issues_model.PullRequestFlowGithub:
isForkPullRequest = pr.IsFromFork()
@@ -351,14 +345,6 @@ func handleWorkflows(
Status: actions_model.StatusWaiting,
}
- if workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content), false); err == nil {
- notifications, err := workflow.Notifications()
- if err != nil {
- log.Error("Notifications: %w", err)
- }
- run.NotifyEmail = notifications
- }
-
need, err := ifNeedApproval(ctx, run, input.Repo, input.Doer)
if err != nil {
log.Error("check if need approval for repo %d with user %d: %v", input.Repo.ID, input.Doer.ID, err)
@@ -378,19 +364,16 @@ func handleWorkflows(
continue
}
- jobs, err := jobParser(dwf.Content, jobparser.WithVars(vars))
+ jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars))
if err != nil {
- run.Status = actions_model.StatusFailure
- log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err)
- jobs = []*jobparser.SingleWorkflow{{
- Name: dwf.EntryName,
- }}
+ log.Error("jobparser.Parse: %v", err)
+ continue
}
// cancel running jobs if the event is push or pull_request_sync
if run.Event == webhook_module.HookEventPush ||
run.Event == webhook_module.HookEventPullRequestSync {
- if err := CancelPreviousJobs(
+ if err := actions_model.CancelPreviousJobs(
ctx,
run.RepoID,
run.Ref,
@@ -521,7 +504,7 @@ func handleSchedules(
log.Error("CountSchedules: %v", err)
return err
} else if count > 0 {
- if err := CleanRepoScheduleTasks(ctx, input.Repo, false); err != nil {
+ if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo, false); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
}
@@ -543,7 +526,7 @@ func handleSchedules(
crons := make([]*actions_model.ActionSchedule, 0, len(detectedWorkflows))
for _, dwf := range detectedWorkflows {
// Check cron job condition. Only working in default branch
- workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content), false)
+ workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content))
if err != nil {
log.Error("ReadWorkflow: %v", err)
continue
diff --git a/services/actions/notifier_helper_test.go b/services/actions/notifier_helper_test.go
index 525103927b..9166dc3b95 100644
--- a/services/actions/notifier_helper_test.go
+++ b/services/actions/notifier_helper_test.go
@@ -8,16 +8,9 @@ import (
actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
- issues_model "forgejo.org/models/issues"
- repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
- user_model "forgejo.org/models/user"
- actions_module "forgejo.org/modules/actions"
- "forgejo.org/modules/git"
- api "forgejo.org/modules/structs"
webhook_module "forgejo.org/modules/webhook"
- "code.forgejo.org/forgejo/runner/v11/act/jobparser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -56,91 +49,3 @@ func Test_SkipPullRequestEvent(t *testing.T) {
unittest.AssertSuccessfulInsert(t, run)
assert.True(t, SkipPullRequestEvent(db.DefaultContext, webhook_module.HookEventPullRequestSync, repoID, commitSHA))
}
-
-func Test_IssueCommentOnForkPullRequestEvent(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
-
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
- require.NoError(t, pr.LoadIssue(db.DefaultContext))
-
- require.True(t, pr.IsFromFork())
-
- commit := &git.Commit{
- ID: git.MustIDFromString("0000000000000000000000000000000000000000"),
- CommitMessage: "test",
- }
- detectedWorkflows := []*actions_module.DetectedWorkflow{
- {
- TriggerEvent: &jobparser.Event{
- Name: "issue_comment",
- },
- },
- }
- input := ¬ifyInput{
- Repo: repo,
- Doer: doer,
- Event: webhook_module.HookEventIssueComment,
- PullRequest: pr,
- Payload: &api.IssueCommentPayload{},
- }
-
- unittest.AssertSuccessfulDelete(t, &actions_model.ActionRun{RepoID: repo.ID})
-
- err := handleWorkflows(db.DefaultContext, detectedWorkflows, commit, input, "")
- require.NoError(t, err)
-
- runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
- RepoID: repo.ID,
- })
- require.NoError(t, err)
- require.Len(t, runs, 1)
-
- assert.Equal(t, webhook_module.HookEventIssueComment, runs[0].Event)
- assert.False(t, runs[0].IsForkPullRequest)
-}
-
-func Test_OpenForkPullRequestEvent(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
-
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
- require.NoError(t, pr.LoadIssue(db.DefaultContext))
-
- require.True(t, pr.IsFromFork())
-
- commit := &git.Commit{
- ID: git.MustIDFromString("0000000000000000000000000000000000000000"),
- CommitMessage: "test",
- }
- detectedWorkflows := []*actions_module.DetectedWorkflow{
- {
- TriggerEvent: &jobparser.Event{
- Name: "pull_request",
- },
- },
- }
- input := ¬ifyInput{
- Repo: repo,
- Doer: doer,
- Event: webhook_module.HookEventPullRequest,
- PullRequest: pr,
- Payload: &api.PullRequestPayload{},
- }
-
- unittest.AssertSuccessfulDelete(t, &actions_model.ActionRun{RepoID: repo.ID})
-
- err := handleWorkflows(db.DefaultContext, detectedWorkflows, commit, input, "")
- require.NoError(t, err)
-
- runs, err := db.Find[actions_model.ActionRun](db.DefaultContext, actions_model.FindRunOptions{
- RepoID: repo.ID,
- })
- require.NoError(t, err)
- require.Len(t, runs, 1)
-
- assert.Equal(t, webhook_module.HookEventPullRequest, runs[0].Event)
- assert.True(t, runs[0].IsForkPullRequest)
-}
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 07ff7b3187..f66a6ca092 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -4,9 +4,7 @@
package actions
import (
- "bytes"
"context"
- "errors"
"fmt"
"time"
@@ -18,10 +16,7 @@ import (
"forgejo.org/modules/timeutil"
webhook_module "forgejo.org/modules/webhook"
- "code.forgejo.org/forgejo/runner/v11/act/jobparser"
- act_model "code.forgejo.org/forgejo/runner/v11/act/model"
- "github.com/robfig/cron/v3"
- "xorm.io/builder"
+ "github.com/nektos/act/pkg/jobparser"
)
// StartScheduleTasks start the task
@@ -60,7 +55,7 @@ func startTasks(ctx context.Context) error {
// cancel running jobs if the event is push
if row.Schedule.Event == webhook_module.HookEventPush {
// cancel running jobs of the same workflow
- if err := CancelPreviousJobs(
+ if err := actions_model.CancelPreviousJobs(
ctx,
row.RepoID,
row.Schedule.Ref,
@@ -84,33 +79,20 @@ func startTasks(ctx context.Context) error {
}
return fmt.Errorf("GetUnit: %w", err)
}
- actionConfig := cfg.ActionsConfig()
- if actionConfig.IsWorkflowDisabled(row.Schedule.WorkflowID) {
+ if cfg.ActionsConfig().IsWorkflowDisabled(row.Schedule.WorkflowID) {
continue
}
- createAndSchedule := func(row *actions_model.ActionScheduleSpec) (cron.Schedule, error) {
- if err := CreateScheduleTask(ctx, row.Schedule); err != nil {
- return nil, fmt.Errorf("CreateScheduleTask: %v", err)
- }
-
- // Parse the spec
- schedule, err := row.Parse()
- if err != nil {
- return nil, fmt.Errorf("Parse(Spec=%v): %v", row.Spec, err)
- }
- return schedule, nil
+ if err := CreateScheduleTask(ctx, row.Schedule); err != nil {
+ log.Error("CreateScheduleTask: %v", err)
+ return err
}
- schedule, err := createAndSchedule(row)
+ // Parse the spec
+ schedule, err := row.Parse()
if err != nil {
- log.Error("RepoID=%v WorkflowID=%v: %v", row.Schedule.RepoID, row.Schedule.WorkflowID, err)
- actionConfig.DisableWorkflow(row.Schedule.WorkflowID)
- if err := repo_model.UpdateRepoUnit(ctx, cfg); err != nil {
- log.Error("RepoID=%v WorkflowID=%v: CreateScheduleTask: %v", row.Schedule.RepoID, row.Schedule.WorkflowID, err)
- return err
- }
- continue
+ log.Error("Parse: %v", err)
+ return err
}
// Update the spec's next run time and previous run time
@@ -156,18 +138,8 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
return err
}
- workflow, err := act_model.ReadWorkflow(bytes.NewReader(cron.Content), false)
- if err != nil {
- return err
- }
- notifications, err := workflow.Notifications()
- if err != nil {
- return err
- }
- run.NotifyEmail = notifications
-
// Parse the workflow specification from the cron schedule
- workflows, err := jobParser(cron.Content, jobparser.WithVars(vars))
+ workflows, err := jobparser.Parse(cron.Content, jobparser.WithVars(vars))
if err != nil {
return err
}
@@ -180,93 +152,3 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule)
// Return nil if no errors occurred
return nil
}
-
-// CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event.
-// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
-func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
- // Find all runs in the specified repository, reference, and workflow with non-final status
- runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
- RepoID: repoID,
- Ref: ref,
- WorkflowID: workflowID,
- TriggerEvent: event,
- Status: []actions_model.Status{actions_model.StatusRunning, actions_model.StatusWaiting, actions_model.StatusBlocked},
- })
- if err != nil {
- return err
- }
-
- // If there are no runs found, there's no need to proceed with cancellation, so return nil.
- if total == 0 {
- return nil
- }
-
- // Iterate over each found run and cancel its associated jobs.
- for _, run := range runs {
- // Find all jobs associated with the current run.
- jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{
- RunID: run.ID,
- })
- if err != nil {
- return err
- }
-
- // Iterate over each job and attempt to cancel it.
- for _, job := range jobs {
- // Skip jobs that are already in a terminal state (completed, cancelled, etc.).
- status := job.Status
- if status.IsDone() {
- continue
- }
-
- // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it.
- if job.TaskID == 0 {
- job.Status = actions_model.StatusCancelled
- job.Stopped = timeutil.TimeStampNow()
-
- // Update the job's status and stopped time in the database.
- n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
- if err != nil {
- return err
- }
-
- // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again.
- if n == 0 {
- return errors.New("job has changed, try again")
- }
-
- // Continue with the next job.
- continue
- }
-
- // If the job has an associated task, try to stop the task, effectively cancelling the job.
- if err := StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
- return err
- }
- }
- }
-
- // Return nil to indicate successful cancellation of all running and waiting jobs.
- return nil
-}
-
-func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository, cancelPreviousJobs bool) error {
- // If actions disabled when there is schedule task, this will remove the outdated schedule tasks
- // There is no other place we can do this because the app.ini will be changed manually
- if err := actions_model.DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
- return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
- }
- if cancelPreviousJobs {
- // cancel running cron jobs of this repository and delete old schedules
- if err := CancelPreviousJobs(
- ctx,
- repo.ID,
- repo.DefaultBranch,
- "",
- webhook_module.HookEventSchedule,
- ); err != nil {
- return fmt.Errorf("CancelPreviousJobs: %v", err)
- }
- }
- return nil
-}
diff --git a/services/actions/schedule_tasks_test.go b/services/actions/schedule_tasks_test.go
deleted file mode 100644
index 31ed5ec813..0000000000
--- a/services/actions/schedule_tasks_test.go
+++ /dev/null
@@ -1,175 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package actions
-
-import (
- "testing"
-
- actions_model "forgejo.org/models/actions"
- "forgejo.org/models/db"
- repo_model "forgejo.org/models/repo"
- "forgejo.org/models/unit"
- "forgejo.org/models/unittest"
- webhook_module "forgejo.org/modules/webhook"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestServiceActions_startTask(t *testing.T) {
- defer unittest.OverrideFixtures("services/actions/TestServiceActions_startTask")()
- require.NoError(t, unittest.PrepareTestDatabase())
-
- // Load fixtures that are corrupted and create one valid scheduled workflow
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
-
- workflowID := "some.yml"
- schedules := []*actions_model.ActionSchedule{
- {
- Title: "scheduletitle1",
- RepoID: repo.ID,
- OwnerID: repo.OwnerID,
- WorkflowID: workflowID,
- TriggerUserID: repo.OwnerID,
- Ref: "branch",
- CommitSHA: "fakeSHA",
- Event: webhook_module.HookEventSchedule,
- EventPayload: "fakepayload",
- Specs: []string{"* * * * *"},
- Content: []byte(
- `
-jobs:
- job2:
- runs-on: ubuntu-latest
- steps:
- - run: true
-`),
- },
- }
-
- require.Equal(t, 2, unittest.GetCount(t, actions_model.ActionScheduleSpec{}))
- require.NoError(t, actions_model.CreateScheduleTask(t.Context(), schedules))
- require.Equal(t, 3, unittest.GetCount(t, actions_model.ActionScheduleSpec{}))
- _, err := db.GetEngine(db.DefaultContext).Exec("UPDATE `action_schedule_spec` SET next = 1")
- require.NoError(t, err)
-
- // After running startTasks an ActionRun row is created for the valid scheduled workflow
- require.Empty(t, unittest.GetCount(t, actions_model.ActionRun{WorkflowID: workflowID}))
- require.NoError(t, startTasks(t.Context()))
- require.NotEmpty(t, unittest.GetCount(t, actions_model.ActionRun{WorkflowID: workflowID}))
-
- // The invalid workflows loaded from the fixtures are disabled
- repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
- actionUnit, err := repo.GetUnit(t.Context(), unit.TypeActions)
- require.NoError(t, err)
- actionConfig := actionUnit.ActionsConfig()
- assert.True(t, actionConfig.IsWorkflowDisabled("workflow2.yml"))
- assert.True(t, actionConfig.IsWorkflowDisabled("workflow1.yml"))
- assert.False(t, actionConfig.IsWorkflowDisabled("some.yml"))
-}
-
-func TestCreateScheduleTask(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: 2})
-
- assertConstant := func(t *testing.T, cron *actions_model.ActionSchedule, run *actions_model.ActionRun) {
- t.Helper()
- assert.Equal(t, cron.Title, run.Title)
- assert.Equal(t, cron.RepoID, run.RepoID)
- assert.Equal(t, cron.OwnerID, run.OwnerID)
- assert.Equal(t, cron.WorkflowID, run.WorkflowID)
- assert.Equal(t, cron.TriggerUserID, run.TriggerUserID)
- assert.Equal(t, cron.Ref, run.Ref)
- assert.Equal(t, cron.CommitSHA, run.CommitSHA)
- assert.Equal(t, cron.Event, run.Event)
- assert.Equal(t, cron.EventPayload, run.EventPayload)
- assert.Equal(t, cron.ID, run.ScheduleID)
- assert.Equal(t, actions_model.StatusWaiting, run.Status)
- }
-
- assertMutable := func(t *testing.T, expected, run *actions_model.ActionRun) {
- t.Helper()
- assert.Equal(t, expected.NotifyEmail, run.NotifyEmail)
- }
-
- testCases := []struct {
- name string
- cron actions_model.ActionSchedule
- want []actions_model.ActionRun
- }{
- {
- name: "simple",
- cron: actions_model.ActionSchedule{
- Title: "scheduletitle1",
- RepoID: repo.ID,
- OwnerID: repo.OwnerID,
- WorkflowID: "some.yml",
- TriggerUserID: repo.OwnerID,
- Ref: "branch",
- CommitSHA: "fakeSHA",
- Event: webhook_module.HookEventSchedule,
- EventPayload: "fakepayload",
- Content: []byte(
- `
-name: test
-on: push
-jobs:
- job2:
- runs-on: ubuntu-latest
- steps:
- - run: true
-`),
- },
- want: []actions_model.ActionRun{
- {
- Title: "scheduletitle1",
- NotifyEmail: false,
- },
- },
- },
- {
- name: "enable-email-notifications is true",
- cron: actions_model.ActionSchedule{
- Title: "scheduletitle2",
- RepoID: repo.ID,
- OwnerID: repo.OwnerID,
- WorkflowID: "some.yml",
- TriggerUserID: repo.OwnerID,
- Ref: "branch",
- CommitSHA: "fakeSHA",
- Event: webhook_module.HookEventSchedule,
- EventPayload: "fakepayload",
- Content: []byte(
- `
-name: test
-enable-email-notifications: true
-on: push
-jobs:
- job2:
- runs-on: ubuntu-latest
- steps:
- - run: true
-`),
- },
- want: []actions_model.ActionRun{
- {
- Title: "scheduletitle2",
- NotifyEmail: true,
- },
- },
- },
- }
- for _, testCase := range testCases {
- t.Run(testCase.name, func(t *testing.T) {
- require.NoError(t, CreateScheduleTask(t.Context(), &testCase.cron))
- require.Equal(t, len(testCase.want), unittest.GetCount(t, actions_model.ActionRun{RepoID: repo.ID}))
- for _, expected := range testCase.want {
- run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{Title: expected.Title})
- assertConstant(t, &testCase.cron, run)
- assertMutable(t, &expected, run)
- }
- unittest.AssertSuccessfulDelete(t, actions_model.ActionRun{RepoID: repo.ID})
- })
- }
-}
diff --git a/services/actions/task.go b/services/actions/task.go
index bb319c7d05..43c8deaa5f 100644
--- a/services/actions/task.go
+++ b/services/actions/task.go
@@ -5,18 +5,14 @@ package actions
import (
"context"
- "errors"
"fmt"
actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
secret_model "forgejo.org/models/secret"
- "forgejo.org/modules/timeutil"
- "forgejo.org/modules/util"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"google.golang.org/protobuf/types/known/structpb"
- "google.golang.org/protobuf/types/known/timestamppb"
)
func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
@@ -109,144 +105,3 @@ func findTaskNeeds(ctx context.Context, taskJob *actions_model.ActionRunJob) (ma
}
return ret, nil
}
-
-func StopTask(ctx context.Context, taskID int64, status actions_model.Status) error {
- if !status.IsDone() {
- return fmt.Errorf("cannot stop task with status %v", status)
- }
- e := db.GetEngine(ctx)
-
- task := &actions_model.ActionTask{}
- if has, err := e.ID(taskID).Get(task); err != nil {
- return err
- } else if !has {
- return util.ErrNotExist
- }
- if task.Status.IsDone() {
- return nil
- }
-
- now := timeutil.TimeStampNow()
- task.Status = status
- task.Stopped = now
- if _, err := UpdateRunJob(ctx, &actions_model.ActionRunJob{
- ID: task.JobID,
- Status: task.Status,
- Stopped: task.Stopped,
- }, nil); err != nil {
- return err
- }
-
- if err := actions_model.UpdateTask(ctx, task, "status", "stopped"); err != nil {
- return err
- }
-
- if err := task.LoadAttributes(ctx); err != nil {
- return err
- }
-
- for _, step := range task.Steps {
- if !step.Status.IsDone() {
- step.Status = status
- if step.Started == 0 {
- step.Started = now
- }
- step.Stopped = now
- }
- if _, err := e.ID(step.ID).Update(step); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// UpdateTaskByState updates the task by the state.
-// It will always update the task if the state is not final, even there is no change.
-// So it will update ActionTask.Updated to avoid the task being judged as a zombie task.
-func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.TaskState) (*actions_model.ActionTask, error) {
- stepStates := map[int64]*runnerv1.StepState{}
- for _, v := range state.Steps {
- stepStates[v.Id] = v
- }
-
- ctx, commiter, err := db.TxContext(ctx)
- if err != nil {
- return nil, err
- }
- defer commiter.Close()
-
- e := db.GetEngine(ctx)
-
- task := &actions_model.ActionTask{}
- if has, err := e.ID(state.Id).Get(task); err != nil {
- return nil, err
- } else if !has {
- return nil, util.ErrNotExist
- } else if runnerID != task.RunnerID {
- return nil, errors.New("invalid runner for task")
- }
-
- if task.Status.IsDone() {
- // the state is final, do nothing
- return task, nil
- }
-
- // state.Result is not unspecified means the task is finished
- if state.Result != runnerv1.Result_RESULT_UNSPECIFIED {
- task.Status = actions_model.Status(state.Result)
- task.Stopped = timeutil.TimeStamp(state.StoppedAt.AsTime().Unix())
- if err := actions_model.UpdateTask(ctx, task, "status", "stopped"); err != nil {
- return nil, err
- }
- if _, err := UpdateRunJob(ctx, &actions_model.ActionRunJob{
- ID: task.JobID,
- Status: task.Status,
- Stopped: task.Stopped,
- }, nil); err != nil {
- return nil, err
- }
- } else {
- // Force update ActionTask.Updated to avoid the task being judged as a zombie task
- task.Updated = timeutil.TimeStampNow()
- if err := actions_model.UpdateTask(ctx, task, "updated"); err != nil {
- return nil, err
- }
- }
-
- if err := task.LoadAttributes(ctx); err != nil {
- return nil, err
- }
-
- for _, step := range task.Steps {
- var result runnerv1.Result
- if v, ok := stepStates[step.Index]; ok {
- result = v.Result
- step.LogIndex = v.LogIndex
- step.LogLength = v.LogLength
- step.Started = convertTimestamp(v.StartedAt)
- step.Stopped = convertTimestamp(v.StoppedAt)
- }
- if result != runnerv1.Result_RESULT_UNSPECIFIED {
- step.Status = actions_model.Status(result)
- } else if step.Started != 0 {
- step.Status = actions_model.StatusRunning
- }
- if _, err := e.ID(step.ID).Update(step); err != nil {
- return nil, err
- }
- }
-
- if err := commiter.Commit(); err != nil {
- return nil, err
- }
-
- return task, nil
-}
-
-func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp {
- if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 {
- return timeutil.TimeStamp(0)
- }
- return timeutil.TimeStamp(timestamp.AsTime().Unix())
-}
diff --git a/services/actions/variables.go b/services/actions/variables.go
index a9f42105a3..fed1fd0890 100644
--- a/services/actions/variables.go
+++ b/services/actions/variables.go
@@ -87,7 +87,7 @@ func GetVariable(ctx context.Context, opts actions_model.FindVariablesOpts) (*ac
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
- forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI$")
+ forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
)
func envNameCIRegexMatch(name string) error {
diff --git a/services/actions/variables_test.go b/services/actions/variables_test.go
deleted file mode 100644
index f69bc674e1..0000000000
--- a/services/actions/variables_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package actions
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestServicesAction_envNameCIRegexMatch(t *testing.T) {
- require.ErrorContains(t, envNameCIRegexMatch("ci"), "cannot be ci")
- require.ErrorContains(t, envNameCIRegexMatch("CI"), "cannot be ci")
- assert.NoError(t, envNameCIRegexMatch("CI_SOMETHING"))
-}
diff --git a/services/actions/workflows.go b/services/actions/workflows.go
index 27d05c9043..7ec7c3abed 100644
--- a/services/actions/workflows.go
+++ b/services/actions/workflows.go
@@ -24,8 +24,8 @@ import (
"forgejo.org/modules/webhook"
"forgejo.org/services/convert"
- "code.forgejo.org/forgejo/runner/v11/act/jobparser"
- act_model "code.forgejo.org/forgejo/runner/v11/act/model"
+ "github.com/nektos/act/pkg/jobparser"
+ act_model "github.com/nektos/act/pkg/model"
)
type InputRequiredErr struct {
@@ -56,7 +56,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
return nil, nil, err
}
- wf, err := act_model.ReadWorkflow(bytes.NewReader(content), false)
+ wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
return nil, nil, err
}
@@ -111,11 +111,6 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
return nil, nil, err
}
- notifications, err := wf.Notifications()
- if err != nil {
- return nil, nil, err
- }
-
run := &actions_model.ActionRun{
Title: title,
RepoID: repo.ID,
@@ -130,7 +125,6 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
EventPayload: string(p),
TriggerEvent: string(webhook.HookEventWorkflowDispatch),
Status: actions_model.StatusWaiting,
- NotifyEmail: notifications,
}
vars, err := actions_model.GetVariablesOfRun(ctx, run)
@@ -138,7 +132,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
return nil, nil, err
}
- jobs, err := jobParser(content, jobparser.WithVars(vars))
+ jobs, err := jobparser.Parse(content, jobparser.WithVars(vars))
if err != nil {
return nil, nil, err
}
diff --git a/services/agit/agit.go b/services/agit/agit.go
index 8ef641629a..20e87642c3 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -221,7 +221,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
}
// Validate pull request.
- pull_service.ValidatePullRequest(ctx, pr, opts.NewCommitIDs[i], oldCommitID, pusher)
+ pull_service.ValidatePullRequest(ctx, pr, oldCommitID, opts.NewCommitIDs[i], pusher)
// TODO: call `InvalidateCodeComments`
diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go
index 527f6edd92..0030523b22 100644
--- a/services/asymkey/sign.go
+++ b/services/asymkey/sign.go
@@ -10,6 +10,7 @@ import (
asymkey_model "forgejo.org/models/asymkey"
"forgejo.org/models/auth"
+ "forgejo.org/models/db"
git_model "forgejo.org/models/git"
issues_model "forgejo.org/models/issues"
repo_model "forgejo.org/models/repo"
@@ -89,13 +90,6 @@ func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
return "", nil
}
- if setting.Repository.Signing.Format == "ssh" {
- return setting.Repository.Signing.SigningKey, &git.Signature{
- Name: setting.Repository.Signing.SigningName,
- Email: setting.Repository.Signing.SigningEmail,
- }
- }
-
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
// Can ignore the error here as it means that commit.gpgsign is not set
value, _, _ := git.NewCommand(ctx, "config", "--get", "commit.gpgsign").RunStdString(&git.RunOpts{Dir: repoPath})
@@ -151,19 +145,22 @@ Loop:
case always:
break Loop
case pubkey:
- hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
if err != nil {
return false, "", nil, err
}
- if !hasPubKey {
+ if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
- if err != nil {
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
- if !hasTwoFactor {
+ if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
}
@@ -188,19 +185,22 @@ Loop:
case always:
break Loop
case pubkey:
- hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
if err != nil {
return false, "", nil, err
}
- if !hasPubKey {
+ if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
- if err != nil {
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
- if !hasTwoFactor {
+ if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
case parentSigned:
@@ -241,19 +241,22 @@ Loop:
case always:
break Loop
case pubkey:
- hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
if err != nil {
return false, "", nil, err
}
- if !hasPubKey {
+ if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
- if err != nil {
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
- if !hasTwoFactor {
+ if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
case parentSigned:
@@ -303,19 +306,22 @@ Loop:
case always:
break Loop
case pubkey:
- hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
+ keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
+ OwnerID: u.ID,
+ IncludeSubKeys: true,
+ })
if err != nil {
return false, "", nil, err
}
- if !hasPubKey {
+ if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
- if err != nil {
+ twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
+ if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
- if !hasTwoFactor {
+ if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
case approved:
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index b6f763842b..365bd7faf6 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -51,7 +51,7 @@ func NewExternalAttachment(ctx context.Context, attach *repo_model.Attachment) (
if attach.ExternalURL == "" {
return nil, fmt.Errorf("attachment %s should have a external url", attach.Name)
}
- if !validation.IsValidReleaseAssetURL(attach.ExternalURL) {
+ if !validation.IsValidExternalURL(attach.ExternalURL) {
return nil, repo_model.ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
}
diff --git a/services/attachment/attachment_test.go b/services/attachment/attachment_test.go
index ef002bf16c..70b1e80d6a 100644
--- a/services/attachment/attachment_test.go
+++ b/services/attachment/attachment_test.go
@@ -43,6 +43,6 @@ func TestUploadAttachment(t *testing.T) {
attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attach.UUID)
require.NoError(t, err)
- assert.Equal(t, user.ID, attachment.UploaderID)
+ assert.EqualValues(t, user.ID, attachment.UploaderID)
assert.Equal(t, int64(0), attachment.DownloadCount)
}
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 85121b2d7f..85c9296ced 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -77,7 +77,6 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
_ = sess.Delete("openid_determined_username")
_ = sess.Delete("twofaUid")
_ = sess.Delete("twofaRemember")
- _ = sess.Delete("twofaOpenID")
_ = sess.Delete("webauthnAssertion")
_ = sess.Delete("linkAccount")
err = sess.Set("uid", user.ID)
diff --git a/services/auth/basic.go b/services/auth/basic.go
index 4ffe712744..f259ad5f69 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -151,7 +151,6 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
log.Trace("Basic Authorization: Logged in user %-v", u)
- store.GetData()["IsPasswordLogin"] = true
return u, nil
}
diff --git a/services/auth/httpsign.go b/services/auth/httpsign.go
index e776ccbbed..d3cbb8aa60 100644
--- a/services/auth/httpsign.go
+++ b/services/auth/httpsign.go
@@ -134,7 +134,7 @@ func VerifyCert(r *http.Request) (*asymkey_model.PublicKey, error) {
// Check if it's really a ssh certificate
cert, ok := pk.(*ssh.Certificate)
if !ok {
- return nil, errors.New("no certificate found")
+ return nil, fmt.Errorf("no certificate found")
}
c := &ssh.CertChecker{
@@ -153,7 +153,7 @@ func VerifyCert(r *http.Request) (*asymkey_model.PublicKey, error) {
// check the CA of the cert
if !c.IsUserAuthority(cert.SignatureKey) {
- return nil, errors.New("CA check failed")
+ return nil, fmt.Errorf("CA check failed")
}
// Create a verifier
@@ -191,7 +191,7 @@ func VerifyCert(r *http.Request) (*asymkey_model.PublicKey, error) {
}
// No public key matching a principal in the certificate is registered in gitea
- return nil, errors.New("no valid principal found")
+ return nil, fmt.Errorf("no valid principal found")
}
// doVerify iterates across the provided public keys attempting the verify the current request against each key in turn
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index fa13c20a7f..e6d556d10b 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -17,7 +17,6 @@ import (
"forgejo.org/modules/log"
"forgejo.org/modules/setting"
"forgejo.org/modules/timeutil"
- "forgejo.org/modules/util"
"forgejo.org/modules/web/middleware"
"forgejo.org/services/actions"
"forgejo.org/services/auth/source/oauth2"
@@ -138,7 +137,7 @@ func parseToken(req *http.Request) (string, bool) {
// check header token
if auHead := req.Header.Get("Authorization"); auHead != "" {
auths := strings.Fields(auHead)
- if len(auths) == 2 && (util.ASCIIEqualFold(auths[0], "token") || util.ASCIIEqualFold(auths[0], "bearer")) {
+ if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
return auths[1], true
}
}
diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go
index 3fce7df50b..d6455b33ad 100644
--- a/services/auth/oauth2_test.go
+++ b/services/auth/oauth2_test.go
@@ -4,7 +4,6 @@
package auth
import (
- "net/http"
"testing"
"forgejo.org/models/unittest"
@@ -53,30 +52,3 @@ func TestCheckTaskIsRunning(t *testing.T) {
})
}
}
-
-func TestParseToken(t *testing.T) {
- cases := map[string]struct {
- Header string
- ExpectedToken string
- Expected bool
- }{
- "Token Uppercase": {Header: "Token 1234567890123456789012345687901325467890", ExpectedToken: "1234567890123456789012345687901325467890", Expected: true},
- "Token Lowercase": {Header: "token 1234567890123456789012345687901325467890", ExpectedToken: "1234567890123456789012345687901325467890", Expected: true},
- "Token Unicode": {Header: "to\u212Aen 1234567890123456789012345687901325467890", ExpectedToken: "", Expected: false},
- "Bearer Uppercase": {Header: "Bearer 1234567890123456789012345687901325467890", ExpectedToken: "1234567890123456789012345687901325467890", Expected: true},
- "Bearer Lowercase": {Header: "bearer 1234567890123456789012345687901325467890", ExpectedToken: "1234567890123456789012345687901325467890", Expected: true},
- "Missing type": {Header: "1234567890123456789012345687901325467890", ExpectedToken: "", Expected: false},
- "Three Parts": {Header: "abc 1234567890 test", ExpectedToken: "", Expected: false},
- }
-
- for name := range cases {
- c := cases[name]
- t.Run(name, func(t *testing.T) {
- req, _ := http.NewRequest("GET", "/", nil)
- req.Header.Add("Authorization", c.Header)
- ActualToken, ActualSuccess := parseToken(req)
- assert.Equal(t, c.ExpectedToken, ActualToken)
- assert.Equal(t, c.Expected, ActualSuccess)
- })
- }
-}
diff --git a/services/auth/reverseproxy_test.go b/services/auth/reverseproxy_test.go
index cdcd845148..70ce1f8b0b 100644
--- a/services/auth/reverseproxy_test.go
+++ b/services/auth/reverseproxy_test.go
@@ -38,10 +38,10 @@ func TestReverseProxyAuth(t *testing.T) {
require.EqualValues(t, 1, user_model.CountUsers(db.DefaultContext, nil))
unittest.AssertExistsAndLoadBean(t, &user_model.User{Email: "edgar@example.org", Name: "Edgar", LowerName: "edgar", FullName: "Edgar Allan Poe", IsAdmin: true})
- require.Equal(t, "edgar@example.org", user.Email)
- require.Equal(t, "Edgar", user.Name)
- require.Equal(t, "edgar", user.LowerName)
- require.Equal(t, "Edgar Allan Poe", user.FullName)
+ require.EqualValues(t, "edgar@example.org", user.Email)
+ require.EqualValues(t, "Edgar", user.Name)
+ require.EqualValues(t, "edgar", user.LowerName)
+ require.EqualValues(t, "Edgar Allan Poe", user.FullName)
require.True(t, user.IsAdmin)
})
@@ -58,10 +58,10 @@ func TestReverseProxyAuth(t *testing.T) {
require.EqualValues(t, 2, user_model.CountUsers(db.DefaultContext, nil))
unittest.AssertExistsAndLoadBean(t, &user_model.User{Email: "gusted@example.org", Name: "Gusted", LowerName: "gusted", FullName: "â¤â€¿â¤"}, "is_admin = false")
- require.Equal(t, "gusted@example.org", user.Email)
- require.Equal(t, "Gusted", user.Name)
- require.Equal(t, "gusted", user.LowerName)
- require.Equal(t, "â¤â€¿â¤", user.FullName)
+ require.EqualValues(t, "gusted@example.org", user.Email)
+ require.EqualValues(t, "Gusted", user.Name)
+ require.EqualValues(t, "gusted", user.LowerName)
+ require.EqualValues(t, "â¤â€¿â¤", user.FullName)
require.False(t, user.IsAdmin)
})
}
diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go
index b1d8eae6ae..7c18540a10 100644
--- a/services/auth/source/db/authenticate.go
+++ b/services/auth/source/db/authenticate.go
@@ -50,7 +50,7 @@ func Authenticate(ctx context.Context, user *user_model.User, login, password st
if !user.IsPasswordSet() {
return nil, ErrUserPasswordNotSet{UID: user.ID, Name: user.Name}
- } else if !user.ValidatePassword(ctx, password) {
+ } else if !user.ValidatePassword(password) {
return nil, ErrUserPasswordInvalid{UID: user.ID, Name: user.Name}
}
diff --git a/services/auth/source/oauth2/jwtsigningkey_test.go b/services/auth/source/oauth2/jwtsigningkey_test.go
index 9b07b022df..7cf2833696 100644
--- a/services/auth/source/oauth2/jwtsigningkey_test.go
+++ b/services/auth/source/oauth2/jwtsigningkey_test.go
@@ -30,7 +30,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
block, _ := pem.Decode(fileContent)
assert.NotNil(t, block)
- assert.Equal(t, "PRIVATE KEY", block.Type)
+ assert.EqualValues(t, "PRIVATE KEY", block.Type)
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
require.NoError(t, err)
@@ -44,14 +44,14 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
- assert.Equal(t, 2048, rsaPrivateKey.N.BitLen())
+ assert.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
t.Run("Load key with differ specified algorithm", func(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "EdDSA")()
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
- assert.Equal(t, 2048, rsaPrivateKey.N.BitLen())
+ assert.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
})
})
@@ -62,7 +62,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
- assert.Equal(t, 3072, rsaPrivateKey.N.BitLen())
+ assert.EqualValues(t, 3072, rsaPrivateKey.N.BitLen())
})
t.Run("RSA-4096", func(t *testing.T) {
@@ -72,7 +72,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
- assert.Equal(t, 4096, rsaPrivateKey.N.BitLen())
+ assert.EqualValues(t, 4096, rsaPrivateKey.N.BitLen())
})
t.Run("ECDSA-256", func(t *testing.T) {
@@ -82,7 +82,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
parsedKey := loadKey(t)
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
- assert.Equal(t, 256, ecdsaPrivateKey.Params().BitSize)
+ assert.EqualValues(t, 256, ecdsaPrivateKey.Params().BitSize)
})
t.Run("ECDSA-384", func(t *testing.T) {
@@ -92,7 +92,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
parsedKey := loadKey(t)
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
- assert.Equal(t, 384, ecdsaPrivateKey.Params().BitSize)
+ assert.EqualValues(t, 384, ecdsaPrivateKey.Params().BitSize)
})
t.Run("ECDSA-512", func(t *testing.T) {
@@ -102,7 +102,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
parsedKey := loadKey(t)
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
- assert.Equal(t, 521, ecdsaPrivateKey.Params().BitSize)
+ assert.EqualValues(t, 521, ecdsaPrivateKey.Params().BitSize)
})
t.Run("EdDSA", func(t *testing.T) {
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index a3126cf353..5245f88270 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -29,7 +29,6 @@ type Source struct {
GroupTeamMapRemoval bool
RestrictedGroup string
SkipLocalTwoFA bool `json:",omitempty"`
- AllowUsernameChange bool
// reference to the authSource
authSource *auth.Source
diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go
index b060b6b746..fba1fd8a01 100644
--- a/services/auth/source/oauth2/token.go
+++ b/services/auth/source/oauth2/token.go
@@ -4,7 +4,6 @@
package oauth2
import (
- "errors"
"fmt"
"time"
@@ -52,12 +51,12 @@ func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) {
return nil, err
}
if !parsedToken.Valid {
- return nil, errors.New("invalid token")
+ return nil, fmt.Errorf("invalid token")
}
var token *Token
var ok bool
if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
- return nil, errors.New("invalid token")
+ return nil, fmt.Errorf("invalid token")
}
return token, nil
}
diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go
index 0cdc113379..51a14edd9a 100644
--- a/services/automerge/automerge.go
+++ b/services/automerge/automerge.go
@@ -32,7 +32,7 @@ func Init() error {
shared_automerge.PRAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
if shared_automerge.PRAutoMergeQueue == nil {
- return errors.New("unable to create pr_auto_merge queue")
+ return fmt.Errorf("unable to create pr_auto_merge queue")
}
go graceful.GetManager().RunWithCancel(shared_automerge.PRAutoMergeQueue)
return nil
@@ -107,7 +107,6 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
return
}
if !exists {
- log.Trace("GetScheduledMergeByPullID found nothing for PR %d", pullID)
return
}
@@ -162,7 +161,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
return
}
case issues_model.PullRequestFlowAGit:
- headBranchExist := baseGitRepo.IsReferenceExist(pr.GetGitRefName())
+ headBranchExist := git.IsReferenceExist(ctx, baseGitRepo.Path, pr.GetGitRefName())
if !headBranchExist {
log.Warn("Head branch of auto merge %-v does not exist [HeadRepoID: %d, Branch(Agit): %s]", pr, pr.HeadRepoID, pr.HeadBranch)
return
@@ -205,10 +204,6 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
return
}
- if err := pull_model.DeleteScheduledAutoMerge(ctx, pr.ID); err != nil && !db.IsErrNotExist(err) {
- log.Error("DeleteScheduledAutoMerge[%d]: %v", pr.ID, err)
- }
-
if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message, true); err != nil {
log.Error("pull_service.Merge: %v", err)
// FIXME: if merge failed, we should display some error message to the pull request page.
diff --git a/services/context/api.go b/services/context/api.go
index e9f67c720d..37f0e0f559 100644
--- a/services/context/api.go
+++ b/services/context/api.go
@@ -6,7 +6,6 @@ package context
import (
"context"
- "errors"
"fmt"
"net/http"
"net/url"
@@ -187,7 +186,7 @@ func (ctx *APIContext) Error(status int, title string, obj any) {
if status == http.StatusInternalServerError {
log.ErrorWithSkip(1, "%s: %s", title, message)
- if setting.IsProd && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
+ if setting.IsProd && !(ctx.Doer != nil && ctx.Doer.IsAdmin) {
message = ""
}
}
@@ -286,8 +285,8 @@ func APIContexter() func(http.Handler) http.Handler {
}
defer baseCleanUp()
- ctx.AppendContextValue(apiContextKey, ctx)
- ctx.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
+ ctx.Base.AppendContextValue(apiContextKey, ctx)
+ ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
@@ -335,7 +334,7 @@ func (ctx *APIContext) NotFound(objs ...any) {
func ReferencesGitRepo(allowEmpty ...bool) func(ctx *APIContext) (cancel context.CancelFunc) {
return func(ctx *APIContext) (cancel context.CancelFunc) {
// Empty repository does not have reference information.
- if ctx.Repo.Repository.IsEmpty && (len(allowEmpty) == 0 || !allowEmpty[0]) {
+ if ctx.Repo.Repository.IsEmpty && !(len(allowEmpty) != 0 && allowEmpty[0]) {
return nil
}
@@ -366,12 +365,12 @@ func RepoRefForAPI(next http.Handler) http.Handler {
ctx := GetAPIContext(req)
if ctx.Repo.Repository.IsEmpty {
- ctx.NotFound(errors.New("repository is empty"))
+ ctx.NotFound(fmt.Errorf("repository is empty"))
return
}
if ctx.Repo.GitRepo == nil {
- ctx.InternalServerError(errors.New("no open git repo"))
+ ctx.InternalServerError(fmt.Errorf("no open git repo"))
return
}
diff --git a/services/context/api_test.go b/services/context/api_test.go
index 4bc89939ca..90e4d5ec65 100644
--- a/services/context/api_test.go
+++ b/services/context/api_test.go
@@ -9,14 +9,13 @@ import (
"testing"
"forgejo.org/modules/setting"
- "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenAPILinks(t *testing.T) {
- defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")()
+ setting.AppURL = "http://localhost:3000/"
kases := map[string][]string{
"api/v1/repos/jerrykan/example-repo/issues?state=all": {
`; rel="next"`,
@@ -47,6 +46,6 @@ func TestGenAPILinks(t *testing.T) {
links := genAPILinks(u, 100, 20, curPage)
- assert.Equal(t, links, response)
+ assert.EqualValues(t, links, response)
}
}
diff --git a/services/context/base.go b/services/context/base.go
index dc3d226bb0..0275ea8a99 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -250,7 +250,7 @@ func (b *Base) PlainText(status int, text string) {
// Redirect redirects the request
func (b *Base) Redirect(location string, status ...int) {
code := http.StatusSeeOther
- if len(status) == 1 && status[0] > 0 {
+ if len(status) == 1 {
code = status[0]
}
diff --git a/services/context/base_test.go b/services/context/base_test.go
index 9e058d8f24..868ac00f8b 100644
--- a/services/context/base_test.go
+++ b/services/context/base_test.go
@@ -9,13 +9,11 @@ import (
"testing"
"forgejo.org/modules/setting"
- "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
)
func TestRedirect(t *testing.T) {
- defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")()
req, _ := http.NewRequest("GET", "/", nil)
cases := []struct {
@@ -36,7 +34,6 @@ func TestRedirect(t *testing.T) {
cleanup()
has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
assert.Equal(t, c.keep, has, "url = %q", c.url)
- assert.Equal(t, http.StatusSeeOther, resp.Code)
}
req, _ = http.NewRequest("GET", "/", nil)
@@ -48,24 +45,3 @@ func TestRedirect(t *testing.T) {
assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
assert.Equal(t, http.StatusNoContent, resp.Code)
}
-
-func TestRedirectOptionalStatus(t *testing.T) {
- defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")()
- req, _ := http.NewRequest("GET", "/", nil)
-
- cases := []struct {
- expected int
- actual int
- }{
- {expected: 303},
- {http.StatusTemporaryRedirect, 307},
- {http.StatusPermanentRedirect, 308},
- }
- for _, c := range cases {
- resp := httptest.NewRecorder()
- b, cleanup := NewBaseContext(resp, req)
- b.Redirect("/", c.actual)
- cleanup()
- assert.Equal(t, c.expected, resp.Code)
- }
-}
diff --git a/services/context/context.go b/services/context/context.go
index 68074964c8..91484c5ba3 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -41,7 +41,7 @@ type Render interface {
type Context struct {
*Base
- TemplateContext *templates.Context
+ TemplateContext TemplateContext
Render Render
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
@@ -64,6 +64,8 @@ type Context struct {
Package *Package
}
+type TemplateContext map[string]any
+
func init() {
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(WebContextKey).(*Context)
@@ -96,11 +98,10 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
return ctx
}
-func NewTemplateContextForWeb(ctx *Context) *templates.Context {
- tmplCtx := templates.NewContext(ctx)
- tmplCtx.Locale = ctx.Locale
- tmplCtx.AvatarUtils = templates.NewAvatarUtils(ctx)
- tmplCtx.Data = ctx.Data
+func NewTemplateContextForWeb(ctx *Context) TemplateContext {
+ tmplCtx := NewTemplateContext(ctx)
+ tmplCtx["Locale"] = ctx.Base.Locale
+ tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
return tmplCtx
}
@@ -120,18 +121,6 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
return ctx
}
-func (ctx *Context) AddPluralStringsToPageData(keys []string) {
- for _, key := range keys {
- array, fallback := ctx.Locale.TrPluralStringAllForms(key)
-
- ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)[key] = array
-
- if fallback != nil {
- ctx.PageData["PLURALSTRINGS_FALLBACK"].(map[string][]string)[key] = fallback
- }
- }
-}
-
// Contexter initializes a classic context for a request.
func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
@@ -162,8 +151,8 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData
- ctx.AppendContextValue(WebContextKey, ctx)
- ctx.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
+ ctx.Base.AppendContextValue(WebContextKey, ctx)
+ ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
ctx.Csrf = NewCSRFProtector(csrfOpts)
@@ -219,25 +208,6 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.Data["AllLangs"] = translation.AllLangs()
- ctx.PageData["PLURAL_RULE_LANG"] = translation.GetPluralRule(ctx.Locale)
- ctx.PageData["PLURAL_RULE_FALLBACK"] = translation.GetDefaultPluralRule()
- ctx.PageData["PLURALSTRINGS_LANG"] = map[string][]string{}
- ctx.PageData["PLURALSTRINGS_FALLBACK"] = map[string][]string{}
-
- ctx.AddPluralStringsToPageData([]string{"relativetime.mins", "relativetime.hours", "relativetime.days", "relativetime.weeks", "relativetime.months", "relativetime.years"})
-
- ctx.PageData["DATETIMESTRINGS"] = map[string]string{
- "FUTURE": ctx.Locale.TrString("relativetime.future"),
- "NOW": ctx.Locale.TrString("relativetime.now"),
- }
- for _, key := range []string{"relativetime.1day", "relativetime.1week", "relativetime.1month", "relativetime.1year", "relativetime.2days", "relativetime.2weeks", "relativetime.2months", "relativetime.2years"} {
- // These keys are used for special-casing some time words. We only add keys that are actually translated, so that we
- // can fall back to the generic pluralized time word in the correct language if the special case is untranslated.
- if ctx.Locale.HasKey(key) {
- ctx.PageData["DATETIMESTRINGS"].(map[string]string)[key] = ctx.Locale.TrString(key)
- }
- }
-
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
diff --git a/services/context/context_response.go b/services/context/context_response.go
index 386bdd2652..e20e7dd852 100644
--- a/services/context/context_response.go
+++ b/services/context/context_response.go
@@ -1,5 +1,4 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
-// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
@@ -17,7 +16,6 @@ import (
"syscall"
"time"
- "forgejo.org/models/auth"
user_model "forgejo.org/models/user"
"forgejo.org/modules/base"
"forgejo.org/modules/httplib"
@@ -68,10 +66,7 @@ func (ctx *Context) RedirectToFirst(location ...string) string {
return setting.AppSubURL + "/"
}
-const (
- tplStatus404 base.TplName = "status/404"
- tplStatus500 base.TplName = "status/500"
-)
+const tplStatus500 base.TplName = "status/500"
// HTML calls Context.HTML and renders the template to HTTP response
func (ctx *Context) HTML(status int, name base.TplName) {
@@ -130,21 +125,6 @@ func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
ctx.HTML(http.StatusOK, tpl)
}
-// validateTwoFactorRequirement sets ctx-data to hide/show ui-elements depending on the GlobalTwoFactorRequirement
-func (ctx *Context) validateTwoFactorRequirement() {
- if ctx.Doer == nil || !ctx.Doer.MustHaveTwoFactor() {
- return
- }
-
- hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Doer.ID)
- if err != nil {
- log.ErrorWithSkip(2, "Error getting 2fa: %s", err)
- // fallthrough to set the variables
- }
- ctx.Data["MustEnableTwoFactor"] = !hasTwoFactor
- ctx.Data["HideNavbarLinks"] = !hasTwoFactor
-}
-
// NotFound displays a 404 (Not Found) page and prints the given error, if any.
func (ctx *Context) NotFound(logMsg string, logErr error) {
ctx.notFoundInternal(logMsg, logErr)
@@ -172,10 +152,9 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
return
}
- ctx.validateTwoFactorRequirement()
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
- ctx.Data["Title"] = ctx.Locale.TrString("error.not_found.title")
- ctx.HTML(http.StatusNotFound, tplStatus404)
+ ctx.Data["Title"] = "Page Not Found"
+ ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
}
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
@@ -198,7 +177,7 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
}
}
- ctx.validateTwoFactorRequirement()
+ ctx.Data["Title"] = "Internal Server Error"
ctx.HTML(http.StatusInternalServerError, tplStatus500)
}
diff --git a/services/context/context_template.go b/services/context/context_template.go
new file mode 100644
index 0000000000..7878d409ca
--- /dev/null
+++ b/services/context/context_template.go
@@ -0,0 +1,35 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package context
+
+import (
+ "context"
+ "time"
+)
+
+var _ context.Context = TemplateContext(nil)
+
+func NewTemplateContext(ctx context.Context) TemplateContext {
+ return TemplateContext{"_ctx": ctx}
+}
+
+func (c TemplateContext) parentContext() context.Context {
+ return c["_ctx"].(context.Context)
+}
+
+func (c TemplateContext) Deadline() (deadline time.Time, ok bool) {
+ return c.parentContext().Deadline()
+}
+
+func (c TemplateContext) Done() <-chan struct{} {
+ return c.parentContext().Done()
+}
+
+func (c TemplateContext) Err() error {
+ return c.parentContext().Err()
+}
+
+func (c TemplateContext) Value(key any) any {
+ return c.parentContext().Value(key)
+}
diff --git a/services/context/org.go b/services/context/org.go
index c7d06b9bcc..31ad60704f 100644
--- a/services/context/org.go
+++ b/services/context/org.go
@@ -1,6 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2020 The Gitea Authors. All rights reserved.
-// Copyright 2025 The Forgejo Authors. All rights reserved.
+// Copyright 2020 The Gitea Authors.
// SPDX-License-Identifier: MIT
package context
@@ -16,7 +15,6 @@ import (
"forgejo.org/modules/markup/markdown"
"forgejo.org/modules/setting"
"forgejo.org/modules/structs"
- redirect_service "forgejo.org/services/redirect"
)
// Organization contains organization context
@@ -49,13 +47,13 @@ func GetOrganizationByParams(ctx *Context) {
ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
- redirectUserID, err := redirect_service.LookupUserRedirect(ctx, ctx.Doer, orgName)
+ redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName)
if err == nil {
RedirectToUser(ctx.Base, orgName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", err)
} else {
- ctx.ServerError("LookupRedirect", err)
+ ctx.ServerError("LookupUserRedirect", err)
}
} else {
ctx.ServerError("GetUserByName", err)
@@ -167,7 +165,6 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- ctx.Data["IsModerationEnabled"] = setting.Moderation.Enabled
ctx.Data["IsPublicMember"] = func(uid int64) bool {
is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid)
return is
diff --git a/services/context/package.go b/services/context/package.go
index 50ffa8eb7c..e597249e2a 100644
--- a/services/context/package.go
+++ b/services/context/package.go
@@ -97,7 +97,7 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A
return perm.AccessModeNone, nil
}
- if doer != nil && !doer.IsGhost() && !doer.IsAccessAllowed(ctx) {
+ if doer != nil && !doer.IsGhost() && (!doer.IsActive || doer.ProhibitLogin) {
return perm.AccessModeNone, nil
}
@@ -158,7 +158,7 @@ func PackageContexter() func(next http.Handler) http.Handler {
// it is still needed when rendering 500 page in a package handler
ctx := NewWebContext(base, renderer, nil)
- ctx.AppendContextValue(WebContextKey, ctx)
+ ctx.Base.AppendContextValue(WebContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
diff --git a/services/context/private.go b/services/context/private.go
index 94ee31876a..3d7ed694f1 100644
--- a/services/context/private.go
+++ b/services/context/private.go
@@ -67,7 +67,7 @@ func PrivateContexter() func(http.Handler) http.Handler {
base, baseCleanUp := NewBaseContext(w, req)
ctx := &PrivateContext{Base: base}
defer baseCleanUp()
- ctx.AppendContextValue(privateContextKey, ctx)
+ ctx.Base.AppendContextValue(privateContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
diff --git a/services/context/quota.go b/services/context/quota.go
index 502a316107..f6e79e1ebe 100644
--- a/services/context/quota.go
+++ b/services/context/quota.go
@@ -64,7 +64,7 @@ func QuotaRuleAssignmentAPI() func(ctx *APIContext) {
// ctx.CheckQuota checks whether the user in question is within quota limits (web context)
func (ctx *Context) CheckQuota(subject quota_model.LimitSubject, userID int64, username string) bool {
- ok, err := checkQuota(ctx.originCtx, subject, userID, username, func(userID int64, username string) {
+ ok, err := checkQuota(ctx.Base.originCtx, subject, userID, username, func(userID int64, username string) {
showHTML := false
for _, part := range ctx.Req.Header["Accept"] {
if strings.Contains(part, "text/html") {
@@ -91,7 +91,7 @@ func (ctx *Context) CheckQuota(subject quota_model.LimitSubject, userID int64, u
// ctx.CheckQuota checks whether the user in question is within quota limits (API context)
func (ctx *APIContext) CheckQuota(subject quota_model.LimitSubject, userID int64, username string) bool {
- ok, err := checkQuota(ctx.originCtx, subject, userID, username, func(userID int64, username string) {
+ ok, err := checkQuota(ctx.Base.originCtx, subject, userID, username, func(userID int64, username string) {
ctx.JSON(http.StatusRequestEntityTooLarge, APIQuotaExceeded{
Message: "quota exceeded",
UserID: userID,
diff --git a/services/context/repo.go b/services/context/repo.go
index e01e1ddafc..a1e1cadf6c 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -35,7 +35,6 @@ import (
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
asymkey_service "forgejo.org/services/asymkey"
- redirect_service "forgejo.org/services/redirect"
"github.com/editorconfig/editorconfig-core-go/v2"
)
@@ -84,7 +83,7 @@ func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User)
// CanCreateBranch returns true if repository is editable and user has proper access level.
func (r *Repository) CanCreateBranch() bool {
- return r.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
+ return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
}
func (r *Repository) GetObjectFormat() git.ObjectFormat {
@@ -161,12 +160,12 @@ func (r *Repository) CanUseTimetracker(ctx context.Context, issue *issues_model.
// 2. Is the user a contributor, admin, poster or assignee and do the repository policies require this?
isAssigned, _ := issues_model.IsUserAssignedToIssue(ctx, issue, user)
return r.Repository.IsTimetrackerEnabled(ctx) && (!r.Repository.AllowOnlyContributorsToTrackTime(ctx) ||
- r.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
+ r.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
}
// CanCreateIssueDependencies returns whether or not a user can create dependencies.
func (r *Repository) CanCreateIssueDependencies(ctx context.Context, user *user_model.User, isPull bool) bool {
- return r.Repository.IsDependenciesEnabled(ctx) && r.CanWriteIssuesOrPulls(isPull)
+ return r.Repository.IsDependenciesEnabled(ctx) && r.Permission.CanWriteIssuesOrPulls(isPull)
}
// GetCommitsCount returns cached commit count for current view
@@ -362,9 +361,7 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
if ctx.Req.URL.RawQuery != "" {
redirectPath += "?" + ctx.Req.URL.RawQuery
}
- // Git client needs a 301 redirect by default to follow the new location
- // It's not documentated in git documentation, but it's the behavior of git client
- ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusMovedPermanently)
+ ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
}
func repoAssignment(ctx *Context, repo *repo_model.Repository) {
@@ -381,7 +378,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
}
// Check access.
- if !ctx.Repo.HasAccess() {
+ if !ctx.Repo.Permission.HasAccess() {
if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx)
return
@@ -478,12 +475,12 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
return nil
}
- if redirectUserID, err := redirect_service.LookupUserRedirect(ctx, ctx.Doer, userName); err == nil {
+ if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
RedirectToUser(ctx.Base, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", nil)
} else {
- ctx.ServerError("LookupRedirect", err)
+ ctx.ServerError("LookupUserRedirect", err)
}
} else {
ctx.ServerError("GetUserByName", err)
@@ -520,7 +517,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
repo, err := repo_model.GetRepositoryByName(ctx, owner.ID, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
- redirectRepoID, err := redirect_service.LookupRepoRedirect(ctx, ctx.Doer, owner.ID, repoName)
+ redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, repoName)
if err == nil {
RedirectToRepo(ctx.Base, redirectRepoID)
} else if repo_model.IsErrRedirectNotExist(err) {
@@ -594,7 +591,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
ctx.Data["CanWriteIssues"] = ctx.Repo.CanWrite(unit_model.TypeIssues)
ctx.Data["CanWritePulls"] = ctx.Repo.CanWrite(unit_model.TypePullRequests)
ctx.Data["CanWriteActions"] = ctx.Repo.CanWrite(unit_model.TypeActions)
- ctx.Data["IsModerationEnabled"] = setting.Moderation.Enabled
canSignedUserFork, err := repo_module.CanUserForkRepo(ctx, ctx.Doer, ctx.Repo.Repository)
if err != nil {
@@ -645,11 +641,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
ctx.Data["OpenGraphImageURL"] = repo.SummaryCardURL()
ctx.Data["OpenGraphImageWidth"] = cardWidth
ctx.Data["OpenGraphImageHeight"] = cardHeight
- if util.IsEmptyString(repo.Description) {
- ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.summary_card_alt", repo.FullName())
- } else {
- ctx.Data["OpenGraphImageAltText"] = ctx.Tr("og.repo.summary_card.alt_description", repo.FullName(), repo.Description)
- }
+ ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.summary_card_alt", repo.FullName())
if repo.IsFork {
RetrieveBaseRepo(ctx, repo)
diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go
index e71fc50c1f..2fa177e604 100644
--- a/services/context/upload/upload.go
+++ b/services/context/upload/upload.go
@@ -76,15 +76,14 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error {
// AddUploadContext renders template values for dropzone
func AddUploadContext(ctx *context.Context, uploadType string) {
- switch uploadType {
- case "release":
+ if uploadType == "release" {
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/releases/attachments"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/releases/attachments/remove"
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/releases/attachments"
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Release.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
- case "comment":
+ } else if uploadType == "comment" {
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove"
if len(ctx.Params(":index")) > 0 {
@@ -95,7 +94,7 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
- case "repo":
+ } else if uploadType == "repo" {
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove"
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file"
diff --git a/services/context/user.go b/services/context/user.go
index 63260a49aa..a82c90d7a6 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -9,7 +9,6 @@ import (
"strings"
user_model "forgejo.org/models/user"
- redirect_service "forgejo.org/services/redirect"
)
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
@@ -69,12 +68,12 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, string, an
contextUser, err = user_model.GetUserByName(ctx, username)
if err != nil {
if user_model.IsErrUserNotExist(err) {
- if redirectUserID, err := redirect_service.LookupUserRedirect(ctx, doer, username); err == nil {
+ if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
RedirectToUser(ctx, username, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
errCb(http.StatusNotFound, "GetUserByName", err)
} else {
- errCb(http.StatusInternalServerError, "LookupRedirect", err)
+ errCb(http.StatusInternalServerError, "LookupUserRedirect", err)
}
} else {
errCb(http.StatusInternalServerError, "GetUserByName", err)
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index a4e674a896..ebab04f620 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -68,7 +68,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
ctx.PageData = map[string]any{}
ctx.Data["PageStartTime"] = time.Now()
chiCtx := chi.NewRouteContext()
- ctx.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp
}
@@ -83,7 +83,7 @@ func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptes
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
chiCtx := chi.NewRouteContext()
- ctx.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp
}
@@ -96,7 +96,7 @@ func MockPrivateContext(t *testing.T, reqPath string) (*context.PrivateContext,
ctx := &context.PrivateContext{Base: base}
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
chiCtx := chi.NewRouteContext()
- ctx.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp
}
diff --git a/services/contexttest/pagedata_test.go b/services/contexttest/pagedata_test.go
deleted file mode 100644
index 0c9319b6db..0000000000
--- a/services/contexttest/pagedata_test.go
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package contexttest
-
-import (
- "testing"
-
- "forgejo.org/modules/translation"
- "forgejo.org/modules/translation/i18n"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestPluralStringsForClient(t *testing.T) {
- mockLocale := translation.MockLocale{}
- mockLocale.MockTranslations = map[string]string{
- "relativetime.mins" + i18n.PluralFormSeparator + "one": "%d minute ago",
- "relativetime.hours" + i18n.PluralFormSeparator + "one": "%d hour ago",
- "relativetime.days" + i18n.PluralFormSeparator + "one": "%d day ago",
- "relativetime.weeks" + i18n.PluralFormSeparator + "one": "%d week ago",
- "relativetime.months" + i18n.PluralFormSeparator + "one": "%d month ago",
- "relativetime.years" + i18n.PluralFormSeparator + "one": "%d year ago",
- "relativetime.mins" + i18n.PluralFormSeparator + "other": "%d minutes ago",
- "relativetime.hours" + i18n.PluralFormSeparator + "other": "%d hours ago",
- "relativetime.days" + i18n.PluralFormSeparator + "other": "%d days ago",
- "relativetime.weeks" + i18n.PluralFormSeparator + "other": "%d weeks ago",
- "relativetime.months" + i18n.PluralFormSeparator + "other": "%d months ago",
- "relativetime.years" + i18n.PluralFormSeparator + "other": "%d years ago",
- }
-
- ctx, _ := MockContext(t, "/")
- ctx.Locale = mockLocale
- assert.True(t, ctx.Locale.HasKey("relativetime.mins"))
- assert.True(t, ctx.Locale.HasKey("relativetime.weeks"))
- assert.Equal(t, "%d minutes ago", ctx.Locale.TrString("relativetime.mins"+i18n.PluralFormSeparator+"other"))
- assert.Equal(t, "%d week ago", ctx.Locale.TrString("relativetime.weeks"+i18n.PluralFormSeparator+"one"))
-
- assert.Empty(t, ctx.PageData)
- ctx.PageData["PLURALSTRINGS_LANG"] = map[string][]string{}
- assert.Empty(t, ctx.PageData["PLURALSTRINGS_LANG"])
-
- ctx.AddPluralStringsToPageData([]string{"relativetime.mins", "relativetime.hours"})
- assert.Len(t, ctx.PageData["PLURALSTRINGS_LANG"], 2)
- assert.Len(t, ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.mins"], 2)
- assert.Len(t, ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.hours"], 2)
- assert.Equal(t, "%d minute ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.mins"][0])
- assert.Equal(t, "%d minutes ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.mins"][1])
- assert.Equal(t, "%d hour ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.hours"][0])
- assert.Equal(t, "%d hours ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.hours"][1])
-
- ctx.AddPluralStringsToPageData([]string{"relativetime.years", "relativetime.days"})
- assert.Len(t, ctx.PageData["PLURALSTRINGS_LANG"], 4)
- assert.Len(t, ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.mins"], 2)
- assert.Len(t, ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.days"], 2)
- assert.Len(t, ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.years"], 2)
- assert.Equal(t, "%d minute ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.mins"][0])
- assert.Equal(t, "%d minutes ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.mins"][1])
- assert.Equal(t, "%d day ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.days"][0])
- assert.Equal(t, "%d days ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.days"][1])
- assert.Equal(t, "%d year ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.years"][0])
- assert.Equal(t, "%d years ago", ctx.PageData["PLURALSTRINGS_LANG"].(map[string][]string)["relativetime.years"][1])
-}
diff --git a/services/convert/action.go b/services/convert/action.go
deleted file mode 100644
index 703c1f1261..0000000000
--- a/services/convert/action.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package convert
-
-import (
- "context"
-
- actions_model "forgejo.org/models/actions"
- access_model "forgejo.org/models/perm/access"
- user_model "forgejo.org/models/user"
- api "forgejo.org/modules/structs"
-)
-
-// ToActionRun convert actions_model.User to api.ActionRun
-// the run needs all attributes loaded
-func ToActionRun(ctx context.Context, run *actions_model.ActionRun, doer *user_model.User) *api.ActionRun {
- if run == nil {
- return nil
- }
-
- permissionInRepo, _ := access_model.GetUserRepoPermission(ctx, run.Repo, doer)
-
- return &api.ActionRun{
- ID: run.ID,
- Title: run.Title,
- Repo: ToRepo(ctx, run.Repo, permissionInRepo),
- WorkflowID: run.WorkflowID,
- Index: run.Index,
- TriggerUser: ToUser(ctx, run.TriggerUser, doer),
- ScheduleID: run.ScheduleID,
- PrettyRef: run.PrettyRef(),
- IsRefDeleted: run.IsRefDeleted,
- CommitSHA: run.CommitSHA,
- IsForkPullRequest: run.IsForkPullRequest,
- NeedApproval: run.NeedApproval,
- ApprovedBy: run.ApprovedBy,
- Event: run.Event.Event(),
- EventPayload: run.EventPayload,
- TriggerEvent: run.TriggerEvent,
- Status: run.Status.String(),
- Started: run.Started.AsTime(),
- Stopped: run.Stopped.AsTime(),
- Created: run.Created.AsTime(),
- Updated: run.Updated.AsTime(),
- Duration: run.Duration(),
- HTMLURL: run.HTMLURL(),
- }
-}
diff --git a/services/convert/activitypub_person.go b/services/convert/activitypub_person.go
deleted file mode 100644
index 2c05f8c1c0..0000000000
--- a/services/convert/activitypub_person.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package convert
-
-import (
- "context"
-
- "forgejo.org/models/activities"
- user_model "forgejo.org/models/user"
- "forgejo.org/modules/activitypub"
-
- ap "github.com/go-ap/activitypub"
-)
-
-func ToActivityPubPersonFeedItem(item *activities.FederatedUserActivity) ap.Note {
- return ap.Note{
- AttributedTo: ap.IRI(item.ActorURI),
- Content: ap.NaturalLanguageValues{{Value: ap.Content(item.NoteContent), Ref: ap.NilLangRef}},
- ID: ap.IRI(item.NoteURL),
- URL: ap.IRI(item.OriginalNote),
- }
-}
-
-func ToActivityPubPerson(ctx context.Context, user *user_model.User) (*ap.Person, error) {
- link := user.APActorID()
- person := ap.PersonNew(ap.IRI(link))
-
- person.Name = ap.NaturalLanguageValuesNew()
- err := person.Name.Set("en", ap.Content(user.FullName))
- if err != nil {
- return nil, err
- }
-
- person.PreferredUsername = ap.NaturalLanguageValuesNew()
- err = person.PreferredUsername.Set("en", ap.Content(user.Name))
- if err != nil {
- return nil, err
- }
-
- person.URL = ap.IRI(user.HTMLURL())
-
- person.Icon = ap.Image{
- Type: ap.ImageType,
- MediaType: "image/png",
- URL: ap.IRI(user.AvatarLink(ctx)),
- }
-
- person.Inbox = ap.IRI(link + "/inbox")
- person.Outbox = ap.IRI(link + "/outbox")
-
- person.PublicKey.ID = ap.IRI(link + "#main-key")
- person.PublicKey.Owner = ap.IRI(link)
-
- publicKeyPem, err := activitypub.GetPublicKey(ctx, user)
- if err != nil {
- return nil, err
- }
- person.PublicKey.PublicKeyPem = publicKeyPem
-
- return person, nil
-}
diff --git a/services/convert/activitypub_user_action.go b/services/convert/activitypub_user_action.go
deleted file mode 100644
index 9c62e6f25c..0000000000
--- a/services/convert/activitypub_user_action.go
+++ /dev/null
@@ -1,177 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package convert
-
-import (
- "context"
- "fmt"
- "html"
- "net/url"
- "time"
-
- activities_model "forgejo.org/models/activities"
- issues_model "forgejo.org/models/issues"
- fm "forgejo.org/modules/forgefed"
- "forgejo.org/modules/json"
- "forgejo.org/modules/markup"
- "forgejo.org/modules/markup/markdown"
-)
-
-func ActionToForgeUserActivity(ctx context.Context, action *activities_model.Action) (fm.ForgeUserActivity, error) {
- render := func(format string, args ...any) string {
- return fmt.Sprintf(`%s %s`, action.ActUser.HTMLURL(), action.GetActDisplayName(ctx), fmt.Sprintf(format, args...))
- }
- renderIssue := func(issue *issues_model.Issue) string {
- return fmt.Sprintf(`%s#%d `,
- issue.HTMLURL(),
- action.GetRepoPath(ctx),
- issue.Index,
- )
- }
- renderRepo := func() string {
- return fmt.Sprintf(`%s `, action.Repo.HTMLURL(), action.GetRepoPath(ctx))
- }
- renderBranch := func() string {
- return fmt.Sprintf(`%s `, action.GetRefLink(ctx), action.GetBranch())
- }
- renderTag := func() string {
- return fmt.Sprintf(`%s `, action.GetRefLink(ctx), action.GetTag())
- }
-
- makeUserActivity := func(format string, args ...any) (fm.ForgeUserActivity, error) {
- return fm.NewForgeUserActivity(action.ActUser, action.ID, render(format, args...))
- }
-
- switch action.OpType {
- case activities_model.ActionCreateRepo:
- return makeUserActivity("created a new repository: %s", renderRepo())
- case activities_model.ActionRenameRepo:
- return makeUserActivity("renamed a repository: %s", renderRepo())
- case activities_model.ActionStarRepo:
- return makeUserActivity("starred a repository: %s", renderRepo())
- case activities_model.ActionWatchRepo:
- return makeUserActivity("started watching a repository: %s", renderRepo())
- case activities_model.ActionCommitRepo:
- type PushCommit struct {
- Sha1 string
- Message string
- AuthorEmail string
- AuthorName string
- CommitterEmail string
- CommitterName string
- Timestamp time.Time
- }
- type PushCommits struct {
- Commits []*PushCommit
- HeadCommit *PushCommit
- CompareURL string
- Len int
- }
-
- commits := &PushCommits{}
- if err := json.Unmarshal([]byte(action.GetContent()), commits); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- commitsHTML := ""
- renderCommit := func(commit *PushCommit) string {
- return fmt.Sprintf(`%s %s `,
- fmt.Sprintf("%s/commit/%s", action.GetRepoAbsoluteLink(ctx), url.PathEscape(commit.Sha1)),
- commit.Sha1,
- html.EscapeString(commit.Message),
- )
- }
- for _, commit := range commits.Commits {
- commitsHTML += renderCommit(commit)
- }
- return makeUserActivity("pushed to %s at %s: ", renderBranch(), renderRepo(), commitsHTML)
- case activities_model.ActionCreateIssue:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("opened issue %s", renderIssue(action.Issue))
- case activities_model.ActionCreatePullRequest:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("opened pull request %s", renderIssue(action.Issue))
- case activities_model.ActionTransferRepo:
- return makeUserActivity("transferred %s", renderRepo())
- case activities_model.ActionPushTag:
- return makeUserActivity("pushed %s at %s", renderTag(), renderRepo())
- case activities_model.ActionCommentIssue:
- renderedComment, err := markdown.RenderString(&markup.RenderContext{
- Ctx: ctx,
- }, action.Comment.Content)
- if err != nil {
- return fm.ForgeUserActivity{}, err
- }
-
- return makeUserActivity(`commented on %s: %s `,
- action.GetCommentHTMLURL(ctx),
- renderIssue(action.Comment.Issue),
- renderedComment,
- )
- case activities_model.ActionMergePullRequest:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("merged pull request %s", renderIssue(action.Issue))
- case activities_model.ActionCloseIssue:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("closed issue %s", renderIssue(action.Issue))
- case activities_model.ActionReopenIssue:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("reopened issue %s", renderIssue(action.Issue))
- case activities_model.ActionClosePullRequest:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("closed pull request %s", renderIssue(action.Issue))
- case activities_model.ActionReopenPullRequest:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("reopened pull request %s", renderIssue(action.Issue))
- case activities_model.ActionDeleteTag:
- return makeUserActivity("deleted tag %s at %s", action.GetTag(), renderRepo())
- case activities_model.ActionDeleteBranch:
- return makeUserActivity("deleted branch %s at %s", action.GetBranch(), renderRepo())
- case activities_model.ActionApprovePullRequest:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("approved pull request %s", renderIssue(action.Issue))
- case activities_model.ActionRejectPullRequest:
- if err := action.LoadIssue(ctx); err != nil {
- return fm.ForgeUserActivity{}, err
- }
- return makeUserActivity("rejected pull request %s", renderIssue(action.Issue))
- case activities_model.ActionCommentPull:
- renderedComment, err := markdown.RenderString(&markup.RenderContext{
- Ctx: ctx,
- }, action.Comment.Content)
- if err != nil {
- return fm.ForgeUserActivity{}, err
- }
-
- return makeUserActivity(`commented on %s: %s `,
- action.GetCommentHTMLURL(ctx),
- renderIssue(action.Comment.Issue),
- renderedComment,
- )
- case activities_model.ActionMirrorSyncPush:
- case activities_model.ActionMirrorSyncCreate:
- case activities_model.ActionMirrorSyncDelete:
- case activities_model.ActionPublishRelease:
- case activities_model.ActionPullReviewDismissed:
- case activities_model.ActionPullRequestReadyForReview:
- case activities_model.ActionAutoMergePullRequest:
- }
-
- return makeUserActivity("performed an unrecognised action: %s", action.OpType.String())
-}
diff --git a/services/convert/attachment.go b/services/convert/attachment.go
index 74ae7c509c..6617aac906 100644
--- a/services/convert/attachment.go
+++ b/services/convert/attachment.go
@@ -4,9 +4,6 @@
package convert
import (
- "mime"
- "path/filepath"
-
repo_model "forgejo.org/models/repo"
api "forgejo.org/modules/structs"
)
@@ -23,13 +20,9 @@ func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm
return attach.DownloadURL()
}
-// ToWebAttachment converts models.Attachment to api.WebAttachment for API usage
-func ToWebAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.WebAttachment {
- attachment := toAttachment(repo, a, WebAssetDownloadURL)
- return &api.WebAttachment{
- Attachment: attachment,
- MimeType: mime.TypeByExtension(filepath.Ext(attachment.Name)),
- }
+// ToAttachment converts models.Attachment to api.Attachment for API usage
+func ToAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment {
+ return toAttachment(repo, a, WebAssetDownloadURL)
}
// ToAPIAttachment converts models.Attachment to api.Attachment for API usage
diff --git a/services/convert/attachment_test.go b/services/convert/attachment_test.go
deleted file mode 100644
index d7bf0c1ee7..0000000000
--- a/services/convert/attachment_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package convert
-
-import (
- "fmt"
- "testing"
- "time"
-
- repo_model "forgejo.org/models/repo"
- "forgejo.org/models/unittest"
- "forgejo.org/modules/setting"
- api "forgejo.org/modules/structs"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestToWebAttachment(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
- attachment := &repo_model.Attachment{
- ID: 10,
- UUID: "uuidxxx",
- RepoID: 1,
- IssueID: 1,
- ReleaseID: 0,
- UploaderID: 0,
- CommentID: 0,
- Name: "test.png",
- DownloadCount: 90,
- Size: 30,
- NoAutoTime: false,
- CreatedUnix: 9342,
- CustomDownloadURL: "",
- ExternalURL: "",
- }
-
- webAttachment := ToWebAttachment(headRepo, attachment)
-
- assert.NotNil(t, webAttachment)
- assert.Equal(t, &api.WebAttachment{
- Attachment: &api.Attachment{
- ID: 10,
- Name: "test.png",
- Created: time.Unix(9342, 0),
- DownloadCount: 90,
- Size: 30,
- UUID: "uuidxxx",
- DownloadURL: fmt.Sprintf("%sattachments/uuidxxx", setting.AppURL),
- Type: "attachment",
- },
- MimeType: "image/png",
- }, webAttachment)
-}
diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go
index 6a691966b8..e041361737 100644
--- a/services/convert/git_commit.go
+++ b/services/convert/git_commit.go
@@ -15,6 +15,7 @@ import (
api "forgejo.org/modules/structs"
"forgejo.org/modules/util"
ctx "forgejo.org/services/context"
+ "forgejo.org/services/gitdiff"
)
// ToCommitUser convert a git.Signature to an api.CommitUser
@@ -209,15 +210,17 @@ func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep
// Get diff stats for commit
if opts.Stat {
- _, totalAdditions, totalDeletions, err := gitRepo.GetCommitShortStat(commit.ID.String())
+ diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{
+ AfterCommitID: commit.ID.String(),
+ })
if err != nil {
return nil, err
}
res.Stats = &api.CommitStats{
- Total: totalAdditions + totalDeletions,
- Additions: totalAdditions,
- Deletions: totalDeletions,
+ Total: diff.TotalAddition + diff.TotalDeletion,
+ Additions: diff.TotalAddition,
+ Deletions: diff.TotalDeletion,
}
}
diff --git a/services/convert/git_commit_test.go b/services/convert/git_commit_test.go
index 97dff365e6..463b93aac3 100644
--- a/services/convert/git_commit_test.go
+++ b/services/convert/git_commit_test.go
@@ -34,7 +34,7 @@ func TestToCommitMeta(t *testing.T) {
commitMeta := ToCommitMeta(headRepo, tag)
assert.NotNil(t, commitMeta)
- assert.Equal(t, &api.CommitMeta{
+ assert.EqualValues(t, &api.CommitMeta{
SHA: sha1.EmptyObjectID().String(),
URL: util.URLJoin(headRepo.APIURL(), "git/commits", sha1.EmptyObjectID().String()),
Created: time.Unix(0, 0),
diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go
index ea8ad9b7ef..97bacfb229 100644
--- a/services/convert/issue_test.go
+++ b/services/convert/issue_test.go
@@ -24,11 +24,10 @@ func TestLabel_ToLabel(t *testing.T) {
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: label.RepoID})
assert.Equal(t, &api.Label{
- ID: label.ID,
- Name: label.Name,
- Color: "abcdef",
- Description: label.Description,
- URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
+ ID: label.ID,
+ Name: label.Name,
+ Color: "abcdef",
+ URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
}, ToLabel(label, repo, nil))
}
diff --git a/services/convert/mirror.go b/services/convert/mirror.go
index 5a815f3a5c..9e7d2659ab 100644
--- a/services/convert/mirror.go
+++ b/services/convert/mirror.go
@@ -23,6 +23,5 @@ func ToPushMirror(ctx context.Context, pm *repo_model.PushMirror) (*api.PushMirr
Interval: pm.Interval.String(),
SyncOnCommit: pm.SyncOnCommit,
PublicKey: pm.GetPublicKey(),
- BranchFilter: pm.BranchFilter,
}, nil
}
diff --git a/services/convert/notification.go b/services/convert/notification.go
index 2a69b62e4b..3a4239e0fe 100644
--- a/services/convert/notification.go
+++ b/services/convert/notification.go
@@ -17,7 +17,7 @@ import (
func ToNotificationThread(ctx context.Context, n *activities_model.Notification) *api.NotificationThread {
result := &api.NotificationThread{
ID: n.ID,
- Unread: n.Status != activities_model.NotificationStatusRead && n.Status != activities_model.NotificationStatusPinned,
+ Unread: !(n.Status == activities_model.NotificationStatusRead || n.Status == activities_model.NotificationStatusPinned),
Pinned: n.Status == activities_model.NotificationStatusPinned,
UpdatedAt: n.UpdatedUnix.AsTime(),
URL: n.APIURL(),
diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go
index 97be118a83..08ccc0e1fc 100644
--- a/services/convert/pull_review.go
+++ b/services/convert/pull_review.go
@@ -66,7 +66,7 @@ func ToPullReviewList(ctx context.Context, rl []*issues_model.Review, doer *user
result := make([]*api.PullReview, 0, len(rl))
for i := range rl {
// show pending reviews only for the user who created them
- if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || (!doer.IsAdmin && doer.ID != rl[i].ReviewerID)) {
+ if rl[i].Type == issues_model.ReviewTypePending && (doer == nil || !(doer.IsAdmin || doer.ID == rl[i].ReviewerID)) {
continue
}
r, err := ToPullReview(ctx, rl[i], doer)
diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go
index c0c69fd9ad..3e4875fc60 100644
--- a/services/convert/pull_test.go
+++ b/services/convert/pull_test.go
@@ -29,7 +29,7 @@ func TestPullRequest_APIFormat(t *testing.T) {
require.NoError(t, pr.LoadIssue(db.DefaultContext))
apiPullRequest := ToAPIPullRequest(git.DefaultContext, pr, nil)
assert.NotNil(t, apiPullRequest)
- assert.Equal(t, &structs.PRBranchInfo{
+ assert.EqualValues(t, &structs.PRBranchInfo{
Name: "branch1",
Ref: "refs/pull/2/head",
Sha: "4a357436d925b5c974181ff12a994538ddc5a269",
diff --git a/services/convert/release_test.go b/services/convert/release_test.go
index 1d214f0222..3abd2ff3ef 100644
--- a/services/convert/release_test.go
+++ b/services/convert/release_test.go
@@ -24,6 +24,6 @@ func TestRelease_ToRelease(t *testing.T) {
apiRelease := ToAPIRelease(db.DefaultContext, repo1, release1)
assert.NotNil(t, apiRelease)
assert.EqualValues(t, 1, apiRelease.ID)
- assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1", apiRelease.URL)
- assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1/assets", apiRelease.UploadURL)
+ assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1", apiRelease.URL)
+ assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1/assets", apiRelease.UploadURL)
}
diff --git a/services/convert/user_test.go b/services/convert/user_test.go
index 8a42a9d97d..01ce8101da 100644
--- a/services/convert/user_test.go
+++ b/services/convert/user_test.go
@@ -31,11 +31,11 @@ func TestUser_ToUser(t *testing.T) {
apiUser = toUser(db.DefaultContext, user1, false, false)
assert.False(t, apiUser.IsAdmin)
- assert.Equal(t, api.VisibleTypePublic.String(), apiUser.Visibility)
+ assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility)
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate})
apiUser = toUser(db.DefaultContext, user31, true, true)
assert.False(t, apiUser.IsAdmin)
- assert.Equal(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
+ assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
}
diff --git a/services/convert/utils.go b/services/convert/utils.go
index 70c5f5cc18..3bbd4e39bd 100644
--- a/services/convert/utils.go
+++ b/services/convert/utils.go
@@ -38,8 +38,6 @@ func ToGitServiceType(value string) structs.GitServiceType {
return structs.GitBucketService
case "forgejo":
return structs.ForgejoService
- case "pagure":
- return structs.PagureService
default:
return structs.PlainGitService
}
diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go
index 6c3bf7d938..b464d8bb68 100644
--- a/services/convert/utils_test.go
+++ b/services/convert/utils_test.go
@@ -10,10 +10,10 @@ import (
)
func TestToCorrectPageSize(t *testing.T) {
- assert.Equal(t, 30, ToCorrectPageSize(0))
- assert.Equal(t, 30, ToCorrectPageSize(-10))
- assert.Equal(t, 20, ToCorrectPageSize(20))
- assert.Equal(t, 50, ToCorrectPageSize(100))
+ assert.EqualValues(t, 30, ToCorrectPageSize(0))
+ assert.EqualValues(t, 30, ToCorrectPageSize(-10))
+ assert.EqualValues(t, 20, ToCorrectPageSize(20))
+ assert.EqualValues(t, 50, ToCorrectPageSize(100))
}
func TestToGitServiceType(t *testing.T) {
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 2db6c15370..7fd4c4e1d8 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -46,13 +46,6 @@ type CleanupHookTaskConfig struct {
NumberToKeep int
}
-// CleanupOfflineRunnersConfig represents a cron task with settings to clean up offline-runner
-type CleanupOfflineRunnersConfig struct {
- BaseConfig
- OlderThan time.Duration
- GlobalScopeOnly bool
-}
-
// GetSchedule returns the schedule for the base config
func (b *BaseConfig) GetSchedule() string {
return b.Schedule
diff --git a/services/cron/tasks_actions.go b/services/cron/tasks_actions.go
index 2cd484fa69..a7fd3cd0bc 100644
--- a/services/cron/tasks_actions.go
+++ b/services/cron/tasks_actions.go
@@ -5,7 +5,6 @@ package cron
import (
"context"
- "time"
user_model "forgejo.org/models/user"
"forgejo.org/modules/setting"
@@ -21,7 +20,6 @@ func initActionsTasks() {
registerCancelAbandonedJobs()
registerScheduleTasks()
registerActionsCleanup()
- registerOfflineRunnersCleanup()
}
func registerStopZombieTasks() {
@@ -76,22 +74,3 @@ func registerActionsCleanup() {
return actions_service.Cleanup(ctx)
})
}
-
-func registerOfflineRunnersCleanup() {
- RegisterTaskFatal("cleanup_offline_runners", &CleanupOfflineRunnersConfig{
- BaseConfig: BaseConfig{
- Enabled: false,
- RunAtStart: false,
- Schedule: "@midnight",
- },
- GlobalScopeOnly: true,
- OlderThan: time.Hour * 24,
- }, func(ctx context.Context, _ *user_model.User, cfg Config) error {
- c := cfg.(*CleanupOfflineRunnersConfig)
- return actions_service.CleanupOfflineRunners(
- ctx,
- c.OlderThan,
- c.GlobalScopeOnly,
- )
- })
-}
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
index 5cc5d4eb14..322fe27ca0 100644
--- a/services/cron/tasks_extended.go
+++ b/services/cron/tasks_extended.go
@@ -15,7 +15,6 @@ import (
issue_indexer "forgejo.org/modules/indexer/issues"
"forgejo.org/modules/setting"
"forgejo.org/modules/updatechecker"
- moderation_service "forgejo.org/services/moderation"
repo_service "forgejo.org/services/repository"
archiver_service "forgejo.org/services/repository/archiver"
user_service "forgejo.org/services/user"
@@ -174,35 +173,34 @@ func registerDeleteOldSystemNotices() {
})
}
-type GCLFSConfig struct {
- BaseConfig
- OlderThan time.Duration
- LastUpdatedMoreThanAgo time.Duration
- NumberToCheckPerRepo int64
- ProportionToCheckPerRepo float64
-}
-
func registerGCLFS() {
if !setting.LFS.StartServer {
return
}
+ type GCLFSConfig struct {
+ OlderThanConfig
+ LastUpdatedMoreThanAgo time.Duration
+ NumberToCheckPerRepo int64
+ ProportionToCheckPerRepo float64
+ }
RegisterTaskFatal("gc_lfs", &GCLFSConfig{
- BaseConfig: BaseConfig{
- Enabled: false,
- RunAtStart: false,
- Schedule: "@every 24h",
+ OlderThanConfig: OlderThanConfig{
+ BaseConfig: BaseConfig{
+ Enabled: false,
+ RunAtStart: false,
+ Schedule: "@every 24h",
+ },
+ // Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
+ // and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
+ // an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
+ // changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
+ // objects.
+ //
+ // It is likely that a week is potentially excessive but it should definitely be enough that any
+ // unassociated LFS object is genuinely unassociated.
+ OlderThan: 24 * time.Hour * 7,
},
- // Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
- // and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
- // an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
- // changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
- // objects.
- //
- // It is likely that a week is potentially excessive but it should definitely be enough that any
- // unassociated LFS object is genuinely unassociated.
- OlderThan: 24 * time.Hour * 7,
-
// Only GC things that haven't been looked at in the past 3 days
LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
NumberToCheckPerRepo: 100,
@@ -227,24 +225,6 @@ func registerRebuildIssueIndexer() {
})
}
-func registerRemoveResolvedReports() {
- type ReportConfig struct {
- BaseConfig
- ConfigKeepResolvedReportsFor time.Duration
- }
- RegisterTaskFatal("remove_resolved_reports", &ReportConfig{
- BaseConfig: BaseConfig{
- Enabled: false,
- RunAtStart: false,
- Schedule: "@every 24h",
- },
- ConfigKeepResolvedReportsFor: setting.Moderation.KeepResolvedReportsFor,
- }, func(ctx context.Context, _ *user_model.User, config Config) error {
- reportConfig := config.(*ReportConfig)
- return moderation_service.RemoveResolvedReports(ctx, reportConfig.ConfigKeepResolvedReportsFor)
- })
-}
-
func initExtendedTasks() {
registerDeleteInactiveUsers()
registerDeleteRepositoryArchives()
@@ -260,7 +240,4 @@ func initExtendedTasks() {
registerDeleteOldSystemNotices()
registerGCLFS()
registerRebuildIssueIndexer()
- if setting.Moderation.Enabled {
- registerRemoveResolvedReports()
- }
}
diff --git a/services/cron/tasks_extended_test.go b/services/cron/tasks_extended_test.go
deleted file mode 100644
index 0b05ff9918..0000000000
--- a/services/cron/tasks_extended_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2025 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package cron
-
-import (
- "testing"
- "time"
-
- "forgejo.org/modules/setting"
- "forgejo.org/modules/test"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func Test_GCLFSConfig(t *testing.T) {
- cfg, err := setting.NewConfigProviderFromData(`
-[cron.gc_lfs]
-ENABLED = true
-RUN_AT_START = true
-SCHEDULE = "@every 2h"
-OLDER_THAN = "1h"
-LAST_UPDATED_MORE_THAN_AGO = "7h"
-NUMBER_TO_CHECK_PER_REPO = 10
-PROPORTION_TO_CHECK_PER_REPO = 0.1
-`)
- require.NoError(t, err)
- defer test.MockVariableValue(&setting.CfgProvider, cfg)()
-
- config := &GCLFSConfig{
- BaseConfig: BaseConfig{
- Enabled: false,
- RunAtStart: false,
- Schedule: "@every 24h",
- },
- OlderThan: 24 * time.Hour * 7,
- LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
- NumberToCheckPerRepo: 100,
- ProportionToCheckPerRepo: 0.6,
- }
-
- _, err = setting.GetCronSettings("gc_lfs", config)
- require.NoError(t, err)
- assert.True(t, config.Enabled)
- assert.True(t, config.RunAtStart)
- assert.Equal(t, "@every 2h", config.Schedule)
- assert.Equal(t, 1*time.Hour, config.OlderThan)
- assert.Equal(t, 7*time.Hour, config.LastUpdatedMoreThanAgo)
- assert.Equal(t, int64(10), config.NumberToCheckPerRepo)
- assert.InDelta(t, 0.1, config.ProportionToCheckPerRepo, 0.001)
-}
diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go
index 465a3fc7c0..04a3680ff5 100644
--- a/services/doctor/authorizedkeys.go
+++ b/services/doctor/authorizedkeys.go
@@ -7,7 +7,6 @@ import (
"bufio"
"bytes"
"context"
- "errors"
"fmt"
"os"
"path/filepath"
@@ -78,7 +77,7 @@ func checkAuthorizedKeys(ctx context.Context, logger log.Logger, autofix bool) e
fPath,
"forgejo admin regenerate keys",
"forgejo doctor check --run authorized-keys --fix")
- return errors.New(`authorized_keys is out of date and should be regenerated with "forgejo admin regenerate keys" or "forgejo doctor check --run authorized-keys --fix"`)
+ return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "forgejo admin regenerate keys" or "forgejo doctor check --run authorized-keys --fix"`)
}
logger.Warn("authorized_keys is out of date. Attempting rewrite...")
err = asymkey_model.RewriteAllPublicKeys(ctx)
diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go
index 6fe4c9c5e6..6fcbd90940 100644
--- a/services/doctor/dbconsistency.go
+++ b/services/doctor/dbconsistency.go
@@ -78,14 +78,7 @@ func genericOrphanCheck(name, subject, refobject, joincond string) consistencyCh
func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
// make sure DB version is up-to-date
- ensureUpToDateWrapper := func(e db.Engine) error {
- engine, err := db.GetMasterEngine(e)
- if err != nil {
- return err
- }
- return migrations.EnsureUpToDate(engine)
- }
- if err := db.InitEngineWithMigration(ctx, ensureUpToDateWrapper); err != nil {
+ if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
return err
}
diff --git a/services/doctor/dbversion.go b/services/doctor/dbversion.go
index c0ff22915d..9c02c732e5 100644
--- a/services/doctor/dbversion.go
+++ b/services/doctor/dbversion.go
@@ -9,15 +9,11 @@ import (
"forgejo.org/models/db"
"forgejo.org/models/migrations"
"forgejo.org/modules/log"
-
- "xorm.io/xorm"
)
func checkDBVersion(ctx context.Context, logger log.Logger, autofix bool) error {
logger.Info("Expected database version: %d", migrations.ExpectedDBVersion())
- if err := db.InitEngineWithMigration(ctx, func(eng db.Engine) error {
- return migrations.EnsureUpToDate(eng.(*xorm.Engine))
- }); err != nil {
+ if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
if !autofix {
logger.Critical("Error: %v during ensure up to date", err)
return err
@@ -25,9 +21,7 @@ func checkDBVersion(ctx context.Context, logger log.Logger, autofix bool) error
logger.Warn("Got Error: %v during ensure up to date", err)
logger.Warn("Attempting to migrate to the latest DB version to fix this.")
- err = db.InitEngineWithMigration(ctx, func(eng db.Engine) error {
- return migrations.Migrate(eng.(*xorm.Engine))
- })
+ err = db.InitEngineWithMigration(ctx, migrations.Migrate)
if err != nil {
logger.Critical("Error: %v during migration", err)
}
diff --git a/services/doctor/fix16961_test.go b/services/doctor/fix16961_test.go
index 75f9f206ab..7a83c808c3 100644
--- a/services/doctor/fix16961_test.go
+++ b/services/doctor/fix16961_test.go
@@ -221,7 +221,7 @@ func Test_fixPullRequestsConfig_16961(t *testing.T) {
if gotFixed != tt.wantFixed {
t.Errorf("fixPullRequestsConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
}
- assert.Equal(t, &tt.expected, cfg)
+ assert.EqualValues(t, &tt.expected, cfg)
})
}
}
@@ -265,7 +265,7 @@ func Test_fixIssuesConfig_16961(t *testing.T) {
if gotFixed != tt.wantFixed {
t.Errorf("fixIssuesConfig_16961() = %v, want %v", gotFixed, tt.wantFixed)
}
- assert.Equal(t, &tt.expected, cfg)
+ assert.EqualValues(t, &tt.expected, cfg)
})
}
}
diff --git a/services/doctor/lfs.go b/services/doctor/lfs.go
index fe858605f4..fed127de5d 100644
--- a/services/doctor/lfs.go
+++ b/services/doctor/lfs.go
@@ -5,7 +5,7 @@ package doctor
import (
"context"
- "errors"
+ "fmt"
"time"
"forgejo.org/modules/log"
@@ -27,7 +27,7 @@ func init() {
func garbageCollectLFSCheck(ctx context.Context, logger log.Logger, autofix bool) error {
if !setting.LFS.StartServer {
- return errors.New("LFS support is disabled")
+ return fmt.Errorf("LFS support is disabled")
}
if err := repository.GarbageCollectLFSMetaObjects(ctx, repository.GarbageCollectLFSMetaObjectsOptions{
diff --git a/services/externalaccount/link.go b/services/externalaccount/link.go
index 5672313181..f5d29b5ce5 100644
--- a/services/externalaccount/link.go
+++ b/services/externalaccount/link.go
@@ -5,7 +5,7 @@ package externalaccount
import (
"context"
- "errors"
+ "fmt"
user_model "forgejo.org/models/user"
@@ -23,7 +23,7 @@ type Store interface {
func LinkAccountFromStore(ctx context.Context, store Store, user *user_model.User) error {
gothUser := store.Get("linkAccountGothUser")
if gothUser == nil {
- return errors.New("not in LinkAccount session")
+ return fmt.Errorf("not in LinkAccount session")
}
return LinkAccountToUser(ctx, user, gothUser.(goth.User))
diff --git a/services/f3/driver/asset.go b/services/f3/driver/asset.go
new file mode 100644
index 0000000000..c9d2ecdf2f
--- /dev/null
+++ b/services/f3/driver/asset.go
@@ -0,0 +1,172 @@
+// Copyright Earl Warren
+// Copyright Loïc Dachary
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "os"
+
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/services/attachment"
+
+ "code.forgejo.org/f3/gof3/v3/f3"
+ f3_id "code.forgejo.org/f3/gof3/v3/id"
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+ f3_util "code.forgejo.org/f3/gof3/v3/util"
+ "github.com/google/uuid"
+)
+
+var _ f3_tree.ForgeDriverInterface = &issue{}
+
+type asset struct {
+ common
+
+ forgejoAsset *repo_model.Attachment
+ sha string
+ contentType string
+ downloadFunc f3.DownloadFuncType
+}
+
+func (o *asset) SetNative(asset any) {
+ o.forgejoAsset = asset.(*repo_model.Attachment)
+}
+
+func (o *asset) GetNativeID() string {
+ return fmt.Sprintf("%d", o.forgejoAsset.ID)
+}
+
+func (o *asset) NewFormat() f3.Interface {
+ node := o.GetNode()
+ return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
+}
+
+func (o *asset) ToFormat() f3.Interface {
+ if o.forgejoAsset == nil {
+ return o.NewFormat()
+ }
+
+ return &f3.ReleaseAsset{
+ Common: f3.NewCommon(o.GetNativeID()),
+ Name: o.forgejoAsset.Name,
+ ContentType: o.contentType,
+ Size: o.forgejoAsset.Size,
+ DownloadCount: o.forgejoAsset.DownloadCount,
+ Created: o.forgejoAsset.CreatedUnix.AsTime(),
+ SHA256: o.sha,
+ DownloadURL: o.forgejoAsset.DownloadURL(),
+ DownloadFunc: o.downloadFunc,
+ }
+}
+
+func (o *asset) FromFormat(content f3.Interface) {
+ asset := content.(*f3.ReleaseAsset)
+ o.forgejoAsset = &repo_model.Attachment{
+ ID: f3_util.ParseInt(asset.GetID()),
+ Name: asset.Name,
+ Size: asset.Size,
+ DownloadCount: asset.DownloadCount,
+ CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()),
+ CustomDownloadURL: asset.DownloadURL,
+ }
+ o.contentType = asset.ContentType
+ o.sha = asset.SHA256
+ o.downloadFunc = asset.DownloadFunc
+}
+
+func (o *asset) Get(ctx context.Context) bool {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ id := node.GetID().Int64()
+
+ asset, err := repo_model.GetAttachmentByID(ctx, id)
+ if repo_model.IsErrAttachmentNotExist(err) {
+ return false
+ }
+ if err != nil {
+ panic(fmt.Errorf("asset %v %w", id, err))
+ }
+
+ o.forgejoAsset = asset
+
+ path := o.forgejoAsset.RelativePath()
+
+ {
+ f, err := storage.Attachments.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ hasher := sha256.New()
+ if _, err := io.Copy(hasher, f); err != nil {
+ panic(fmt.Errorf("io.Copy to hasher: %v", err))
+ }
+ o.sha = hex.EncodeToString(hasher.Sum(nil))
+ }
+
+ o.downloadFunc = func() io.ReadCloser {
+ o.Trace("download %s from copy stored in temporary file %s", o.forgejoAsset.DownloadURL, path)
+ f, err := os.Open(path)
+ if err != nil {
+ panic(err)
+ }
+ return f
+ }
+ return true
+}
+
+func (o *asset) Patch(ctx context.Context) {
+ o.Trace("%d", o.forgejoAsset.ID)
+ if _, err := db.GetEngine(ctx).ID(o.forgejoAsset.ID).Cols("name").Update(o.forgejoAsset); err != nil {
+ panic(fmt.Errorf("UpdateAssetCols: %v %v", o.forgejoAsset, err))
+ }
+}
+
+func (o *asset) Put(ctx context.Context) f3_id.NodeID {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ uploader, err := user_model.GetAdminUser(ctx)
+ if err != nil {
+ panic(fmt.Errorf("GetAdminUser %w", err))
+ }
+
+ o.forgejoAsset.UploaderID = uploader.ID
+ o.forgejoAsset.RepoID = f3_tree.GetProjectID(o.GetNode())
+ o.forgejoAsset.ReleaseID = f3_tree.GetReleaseID(o.GetNode())
+ o.forgejoAsset.UUID = uuid.New().String()
+
+ download := o.downloadFunc()
+ defer download.Close()
+
+ _, err = attachment.NewAttachment(ctx, o.forgejoAsset, download, o.forgejoAsset.Size)
+ if err != nil {
+ panic(err)
+ }
+
+ o.Trace("asset created %d", o.forgejoAsset.ID)
+ return f3_id.NewNodeID(o.forgejoAsset.ID)
+}
+
+func (o *asset) Delete(ctx context.Context) {
+ node := o.GetNode()
+ o.Trace("%s", node.GetID())
+
+ if err := repo_model.DeleteAttachment(ctx, o.forgejoAsset, true); err != nil {
+ panic(err)
+ }
+}
+
+func newAsset() generic.NodeDriverInterface {
+ return &asset{}
+}
diff --git a/services/f3/driver/assets.go b/services/f3/driver/assets.go
new file mode 100644
index 0000000000..106d5029f3
--- /dev/null
+++ b/services/f3/driver/assets.go
@@ -0,0 +1,42 @@
+// Copyright Earl Warren
+// Copyright Loïc Dachary
+// SPDX-License-Identifier: MIT
+
+package driver
+
+import (
+ "context"
+ "fmt"
+
+ repo_model "forgejo.org/models/repo"
+
+ f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
+ "code.forgejo.org/f3/gof3/v3/tree/generic"
+)
+
+type assets struct {
+ container
+}
+
+func (o *assets) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
+ if page > 1 {
+ return generic.NewChildrenSlice(0)
+ }
+
+ releaseID := f3_tree.GetReleaseID(o.GetNode())
+
+ release, err := repo_model.GetReleaseByID(ctx, releaseID)
+ if err != nil {
+ panic(fmt.Errorf("GetReleaseByID %v %w", releaseID, err))
+ }
+
+ if err := release.LoadAttributes(ctx); err != nil {
+ panic(fmt.Errorf("error while listing assets: %v", err))
+ }
+
+ return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(release.Attachments...)...)
+}
+
+func newAssets() generic.NodeDriverInterface {
+ return &assets{}
+}
diff --git a/services/f3/driver/attachment.go b/services/f3/driver/attachment.go
deleted file mode 100644
index 64c188d6e0..0000000000
--- a/services/f3/driver/attachment.go
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright Earl Warren
-// Copyright Loïc Dachary
-// SPDX-License-Identifier: MIT
-
-package driver
-
-import (
- "context"
- "crypto/sha256"
- "encoding/hex"
- "fmt"
- "io"
- "os"
-
- "forgejo.org/models/db"
- repo_model "forgejo.org/models/repo"
- user_model "forgejo.org/models/user"
- "forgejo.org/modules/storage"
- "forgejo.org/modules/timeutil"
- forgejo_attachment "forgejo.org/services/attachment"
-
- "code.forgejo.org/f3/gof3/v3/f3"
- f3_id "code.forgejo.org/f3/gof3/v3/id"
- f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
- "code.forgejo.org/f3/gof3/v3/tree/generic"
- f3_util "code.forgejo.org/f3/gof3/v3/util"
- "github.com/google/uuid"
-)
-
-var _ f3_tree.ForgeDriverInterface = &issue{}
-
-type attachment struct {
- common
-
- forgejoAttachment *repo_model.Attachment
- sha string
- contentType string
- downloadFunc f3.DownloadFuncType
-}
-
-func (o *attachment) SetNative(attachment any) {
- o.forgejoAttachment = attachment.(*repo_model.Attachment)
-}
-
-func (o *attachment) GetNativeID() string {
- return fmt.Sprintf("%d", o.forgejoAttachment.ID)
-}
-
-func (o *attachment) NewFormat() f3.Interface {
- node := o.GetNode()
- return node.GetTree().(f3_tree.TreeInterface).NewFormat(node.GetKind())
-}
-
-func (o *attachment) ToFormat() f3.Interface {
- if o.forgejoAttachment == nil {
- return o.NewFormat()
- }
-
- return &f3.Attachment{
- Common: f3.NewCommon(o.GetNativeID()),
- Name: o.forgejoAttachment.Name,
- ContentType: o.contentType,
- Size: o.forgejoAttachment.Size,
- DownloadCount: o.forgejoAttachment.DownloadCount,
- Created: o.forgejoAttachment.CreatedUnix.AsTime(),
- SHA256: o.sha,
- DownloadURL: o.forgejoAttachment.DownloadURL(),
- DownloadFunc: o.downloadFunc,
- }
-}
-
-func (o *attachment) FromFormat(content f3.Interface) {
- attachment := content.(*f3.Attachment)
- o.forgejoAttachment = &repo_model.Attachment{
- ID: f3_util.ParseInt(attachment.GetID()),
- Name: attachment.Name,
- Size: attachment.Size,
- DownloadCount: attachment.DownloadCount,
- CreatedUnix: timeutil.TimeStamp(attachment.Created.Unix()),
- CustomDownloadURL: attachment.DownloadURL,
- }
- o.contentType = attachment.ContentType
- o.sha = attachment.SHA256
- o.downloadFunc = attachment.DownloadFunc
-}
-
-func (o *attachment) Get(ctx context.Context) bool {
- node := o.GetNode()
- o.Trace("%s", node.GetID())
-
- id := node.GetID().Int64()
-
- attachment, err := repo_model.GetAttachmentByID(ctx, id)
- if repo_model.IsErrAttachmentNotExist(err) {
- return false
- }
- if err != nil {
- panic(fmt.Errorf("attachment %v %w", id, err))
- }
-
- o.forgejoAttachment = attachment
-
- path := o.forgejoAttachment.RelativePath()
-
- {
- f, err := storage.Attachments.Open(path)
- if err != nil {
- panic(err)
- }
- hasher := sha256.New()
- if _, err := io.Copy(hasher, f); err != nil {
- panic(fmt.Errorf("io.Copy to hasher: %v", err))
- }
- o.sha = hex.EncodeToString(hasher.Sum(nil))
- }
-
- o.downloadFunc = func() io.ReadCloser {
- o.Trace("download %s from copy stored in temporary file %s", o.forgejoAttachment.DownloadURL, path)
- f, err := os.Open(path)
- if err != nil {
- panic(err)
- }
- return f
- }
- return true
-}
-
-func (o *attachment) Patch(ctx context.Context) {
- o.Trace("%d", o.forgejoAttachment.ID)
- if _, err := db.GetEngine(ctx).ID(o.forgejoAttachment.ID).Cols("name").Update(o.forgejoAttachment); err != nil {
- panic(fmt.Errorf("UpdateAttachmentCols: %v %v", o.forgejoAttachment, err))
- }
-}
-
-func (o *attachment) Put(ctx context.Context) f3_id.NodeID {
- node := o.GetNode()
- o.Trace("%s", node.GetID())
-
- uploader, err := user_model.GetAdminUser(ctx)
- if err != nil {
- panic(fmt.Errorf("GetAdminUser %w", err))
- }
-
- attachable := f3_tree.GetAttachable(o.GetNode())
- attachableID := f3_tree.GetAttachableID(o.GetNode())
-
- switch attachable.GetKind() {
- case f3_tree.KindRelease:
- o.forgejoAttachment.ReleaseID = attachableID
- case f3_tree.KindComment:
- o.forgejoAttachment.CommentID = attachableID
- case f3_tree.KindIssue, f3_tree.KindPullRequest:
- o.forgejoAttachment.IssueID = attachableID
- default:
- panic(fmt.Errorf("unexpected type %s", attachable.GetKind()))
- }
-
- o.forgejoAttachment.UploaderID = uploader.ID
- o.forgejoAttachment.RepoID = f3_tree.GetProjectID(o.GetNode())
- o.forgejoAttachment.UUID = uuid.New().String()
-
- download := o.downloadFunc()
- defer download.Close()
-
- _, err = forgejo_attachment.NewAttachment(ctx, o.forgejoAttachment, download, o.forgejoAttachment.Size)
- if err != nil {
- panic(err)
- }
-
- o.Trace("attachment created %d", o.forgejoAttachment.ID)
- return f3_id.NewNodeID(o.forgejoAttachment.ID)
-}
-
-func (o *attachment) Delete(ctx context.Context) {
- node := o.GetNode()
- o.Trace("%s", node.GetID())
-
- if err := repo_model.DeleteAttachment(ctx, o.forgejoAttachment, true); err != nil {
- panic(err)
- }
-}
-
-func newAttachment() generic.NodeDriverInterface {
- return &attachment{}
-}
diff --git a/services/f3/driver/attachments.go b/services/f3/driver/attachments.go
deleted file mode 100644
index 392afda52c..0000000000
--- a/services/f3/driver/attachments.go
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright Earl Warren
-// Copyright Loïc Dachary
-// SPDX-License-Identifier: MIT
-
-package driver
-
-import (
- "context"
- "fmt"
-
- issues_model "forgejo.org/models/issues"
- repo_model "forgejo.org/models/repo"
-
- f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
- "code.forgejo.org/f3/gof3/v3/tree/generic"
-)
-
-type attachments struct {
- container
-}
-
-func (o *attachments) ListPage(ctx context.Context, page int) generic.ChildrenSlice {
- if page > 1 {
- return generic.NewChildrenSlice(0)
- }
-
- attachable := f3_tree.GetAttachable(o.GetNode())
- attachableID := f3_tree.GetAttachableID(o.GetNode())
-
- var attachments []*repo_model.Attachment
-
- switch attachable.GetKind() {
- case f3_tree.KindRelease:
- release, err := repo_model.GetReleaseByID(ctx, attachableID)
- if err != nil {
- panic(fmt.Errorf("GetReleaseByID %v %w", attachableID, err))
- }
-
- if err := release.LoadAttributes(ctx); err != nil {
- panic(fmt.Errorf("error while listing attachments: %v", err))
- }
-
- attachments = release.Attachments
-
- case f3_tree.KindComment:
- comment, err := issues_model.GetCommentByID(ctx, attachableID)
- if err != nil {
- panic(fmt.Errorf("GetCommentByID %v %w", attachableID, err))
- }
-
- if err := comment.LoadAttachments(ctx); err != nil {
- panic(fmt.Errorf("error while listing attachments: %v", err))
- }
-
- attachments = comment.Attachments
-
- case f3_tree.KindIssue, f3_tree.KindPullRequest:
- repoID := f3_tree.GetProjectID(o.GetNode())
- issue, err := issues_model.GetIssueByIndex(ctx, repoID, attachableID)
- if err != nil {
- panic(fmt.Errorf("GetIssueByID %v %w", attachableID, err))
- }
-
- if err := issue.LoadAttachments(ctx); err != nil {
- panic(fmt.Errorf("error while listing attachments: %v", err))
- }
-
- attachments = issue.Attachments
-
- default:
- panic(fmt.Errorf("unexpected type %s", attachable.GetKind()))
- }
-
- return f3_tree.ConvertListed(ctx, o.GetNode(), f3_tree.ConvertToAny(attachments...)...)
-}
-
-func newAttachments() generic.NodeDriverInterface {
- return &attachments{}
-}
diff --git a/services/f3/driver/reaction.go b/services/f3/driver/reaction.go
index b959206074..74c50b9d13 100644
--- a/services/f3/driver/reaction.go
+++ b/services/f3/driver/reaction.go
@@ -89,7 +89,7 @@ func (o *reaction) Patch(ctx context.Context) {
}
func (o *reaction) Put(ctx context.Context) f3_id.NodeID {
- o.Trace("%v", o.forgejoReaction.User)
+ o.Error("%v", o.forgejoReaction.User)
sess := db.GetEngine(ctx)
@@ -110,7 +110,7 @@ func (o *reaction) Put(ctx context.Context) f3_id.NodeID {
panic(fmt.Errorf("unexpected type %v", reactionable.GetKind()))
}
- o.Trace("%v", o.forgejoReaction)
+ o.Error("%v", o.forgejoReaction)
if _, err := sess.Insert(o.forgejoReaction); err != nil {
panic(err)
diff --git a/services/f3/driver/repository.go b/services/f3/driver/repository.go
index 3cd9aa7f2e..e7f4e43723 100644
--- a/services/f3/driver/repository.go
+++ b/services/f3/driver/repository.go
@@ -72,7 +72,7 @@ func (o *repository) upsert(ctx context.Context) f3_id.NodeID {
return f3_id.NewNodeID(o.f.Name)
}
-func (o *repository) SetFetchFunc(fetchFunc func(ctx context.Context, destination, internalRef string)) {
+func (o *repository) SetFetchFunc(fetchFunc func(ctx context.Context, destination string, internalRefs []string)) {
o.f.FetchFunc = fetchFunc
}
@@ -93,16 +93,10 @@ func (o *repository) GetRepositoryPushURL() string {
return o.getURL()
}
-func (o *repository) GetRepositoryInternalRef() string {
- return ""
+func (o *repository) GetRepositoryInternalRefs() []string {
+ return []string{}
}
-func (o *repository) GetPullRequestBranch(pr *f3.PullRequestBranch) *f3.PullRequestBranch {
- panic("")
-}
-func (o *repository) CreatePullRequestBranch(pr *f3.PullRequestBranch) {}
-func (o *repository) DeletePullRequestBranch(pr *f3.PullRequestBranch) {}
-
func newRepository(_ context.Context) generic.NodeDriverInterface {
r := &repository{
f: &f3.Repository{},
diff --git a/services/f3/driver/tree.go b/services/f3/driver/tree.go
index fe11b15f6e..ff927df9d4 100644
--- a/services/f3/driver/tree.go
+++ b/services/f3/driver/tree.go
@@ -49,10 +49,10 @@ func (o *treeDriver) Factory(ctx context.Context, kind f3_kind.Kind) generic.Nod
return newComments()
case f3_tree.KindComment:
return newComment()
- case f3_tree.KindAttachments:
- return newAttachments()
- case f3_tree.KindAttachment:
- return newAttachment()
+ case f3_tree.KindAssets:
+ return newAssets()
+ case f3_tree.KindAsset:
+ return newAsset()
case f3_tree.KindLabels:
return newLabels()
case f3_tree.KindLabel:
diff --git a/services/f3/util/logger_test.go b/services/f3/util/logger_test.go
index f62d9e2e82..4afd5dd57f 100644
--- a/services/f3/util/logger_test.go
+++ b/services/f3/util/logger_test.go
@@ -23,7 +23,7 @@ func TestF3UtilMessage(t *testing.T) {
actual = fmt.Sprintf(message, args...)
}, nil)
logger.Message("EXPECTED %s", "MESSAGE")
- assert.Equal(t, expected, actual)
+ assert.EqualValues(t, expected, actual)
}
func TestF3UtilLogger(t *testing.T) {
diff --git a/services/federation/delivery_queue.go b/services/federation/delivery_queue.go
deleted file mode 100644
index f71467e9f0..0000000000
--- a/services/federation/delivery_queue.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "fmt"
- "io"
-
- "forgejo.org/models/user"
- "forgejo.org/modules/activitypub"
- "forgejo.org/modules/graceful"
- "forgejo.org/modules/log"
- "forgejo.org/modules/process"
- "forgejo.org/modules/queue"
-)
-
-type deliveryQueueItem struct {
- Doer *user.User
- InboxURL string
- Payload []byte
- DeliveryCount int
-}
-
-var deliveryQueue *queue.WorkerPoolQueue[deliveryQueueItem]
-
-func initDeliveryQueue() error {
- deliveryQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "activitypub_inbox_delivery", deliveryQueueHandler)
- if deliveryQueue == nil {
- return fmt.Errorf("unable to create activitypub_inbox_delivery queue")
- }
- go graceful.GetManager().RunWithCancel(deliveryQueue)
-
- return nil
-}
-
-func deliveryQueueHandler(items ...deliveryQueueItem) (unhandled []deliveryQueueItem) {
- for _, item := range items {
- item.DeliveryCount++
- err := deliverToInbox(item)
- if err != nil && item.DeliveryCount < 10 {
- unhandled = append(unhandled, item)
- }
- }
- return unhandled
-}
-
-func deliverToInbox(item deliveryQueueItem) error {
- ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(),
- fmt.Sprintf("Delivering an Activity via user[%d] (%s), to %s", item.Doer.ID, item.Doer.Name, item.InboxURL))
- defer finished()
-
- clientFactory, err := activitypub.GetClientFactory(ctx)
- if err != nil {
- return err
- }
- apclient, err := clientFactory.WithKeys(ctx, item.Doer, item.Doer.APActorID()+"#main-key")
- if err != nil {
- return err
- }
-
- log.Debug("Delivering %s to %s", item.Payload, item.InboxURL)
- res, err := apclient.Post(item.Payload, item.InboxURL)
- if err != nil {
- return err
- }
- if res.StatusCode >= 400 {
- defer res.Body.Close()
- body, _ := io.ReadAll(io.LimitReader(res.Body, 16*1024))
-
- log.Warn("Delivering to %s failed: %d %s, %v times", item.InboxURL, res.StatusCode, string(body), item.DeliveryCount)
- return fmt.Errorf("delivery failed")
- }
-
- return nil
-}
diff --git a/services/federation/error.go b/services/federation/error.go
deleted file mode 100644
index 425035d0d5..0000000000
--- a/services/federation/error.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "fmt"
- "net/http"
-)
-
-type ErrNotAcceptable struct {
- Message string
-}
-
-func NewErrNotAcceptablef(format string, a ...any) ErrNotAcceptable {
- message := fmt.Sprintf(format, a...)
- return ErrNotAcceptable{Message: message}
-}
-
-func (err ErrNotAcceptable) Error() string {
- return fmt.Sprintf("NotAcceptable: %v", err.Message)
-}
-
-type ErrInternal struct {
- Message string
-}
-
-func NewErrInternalf(format string, a ...any) ErrInternal {
- message := fmt.Sprintf(format, a...)
- return ErrInternal{Message: message}
-}
-
-func (err ErrInternal) Error() string {
- return fmt.Sprintf("InternalServerError: %v", err.Message)
-}
-
-func HTTPStatus(err error) int {
- switch err.(type) {
- case ErrNotAcceptable:
- return http.StatusNotAcceptable
- default:
- return http.StatusInternalServerError
- }
-}
diff --git a/services/federation/federation_service.go b/services/federation/federation_service.go
index ccdb9bbab0..21c7be855b 100644
--- a/services/federation/federation_service.go
+++ b/services/federation/federation_service.go
@@ -1,16 +1,18 @@
-// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package federation
import (
"context"
- "database/sql"
"fmt"
+ "net/http"
"net/url"
"strings"
+ "time"
"forgejo.org/models/forgefed"
+ "forgejo.org/models/repo"
"forgejo.org/models/user"
"forgejo.org/modules/activitypub"
"forgejo.org/modules/auth/password"
@@ -22,24 +24,128 @@ import (
"github.com/google/uuid"
)
-func Init() error {
- if !setting.Federation.Enabled {
- return nil
+// ProcessLikeActivity receives a ForgeLike activity and does the following:
+// Validation of the activity
+// Creation of a (remote) federationHost if not existing
+// Creation of a forgefed Person if not existing
+// Validation of incoming RepositoryID against Local RepositoryID
+// Star the repo if it wasn't already stared
+// Do some mitigation against out of order attacks
+func ProcessLikeActivity(ctx context.Context, form any, repositoryID int64) (int, string, error) {
+ activity := form.(*fm.ForgeLike)
+ if res, err := validation.IsValid(activity); !res {
+ return http.StatusNotAcceptable, "Invalid activity", err
}
- return initDeliveryQueue()
+ log.Info("Activity validated:%v", activity)
+
+ // parse actorID (person)
+ actorURI := activity.Actor.GetID().String()
+ log.Info("actorURI was: %v", actorURI)
+ federationHost, err := GetFederationHostForURI(ctx, actorURI)
+ if err != nil {
+ return http.StatusInternalServerError, "Wrong FederationHost", err
+ }
+ if !activity.IsNewer(federationHost.LatestActivity) {
+ return http.StatusNotAcceptable, "Activity out of order.", fmt.Errorf("Activity already processed")
+ }
+ actorID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName))
+ if err != nil {
+ return http.StatusNotAcceptable, "Invalid PersonID", err
+ }
+ log.Info("Actor accepted:%v", actorID)
+
+ // parse objectID (repository)
+ objectID, err := fm.NewRepositoryID(activity.Object.GetID().String(), string(forgefed.ForgejoSourceType))
+ if err != nil {
+ return http.StatusNotAcceptable, "Invalid objectId", err
+ }
+ if objectID.ID != fmt.Sprint(repositoryID) {
+ return http.StatusNotAcceptable, "Invalid objectId", err
+ }
+ log.Info("Object accepted:%v", objectID)
+
+ // Check if user already exists
+ user, _, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
+ if err != nil {
+ return http.StatusInternalServerError, "Searching for user failed", err
+ }
+ if user != nil {
+ log.Info("Found local federatedUser: %v", user)
+ } else {
+ user, _, err = CreateUserFromAP(ctx, actorID, federationHost.ID)
+ if err != nil {
+ return http.StatusInternalServerError, "Error creating federatedUser", err
+ }
+ log.Info("Created federatedUser from ap: %v", user)
+ }
+ log.Info("Got user:%v", user.Name)
+
+ // execute the activity if the repo was not stared already
+ alreadyStared := repo.IsStaring(ctx, user.ID, repositoryID)
+ if !alreadyStared {
+ err = repo.StarRepo(ctx, user.ID, repositoryID, true)
+ if err != nil {
+ return http.StatusNotAcceptable, "Error staring", err
+ }
+ }
+ federationHost.LatestActivity = activity.StartTime
+ err = forgefed.UpdateFederationHost(ctx, federationHost)
+ if err != nil {
+ return http.StatusNotAcceptable, "Error updating federatedHost", err
+ }
+
+ return 0, "", nil
}
-func FindOrCreateFederationHost(ctx context.Context, actorURI string) (*forgefed.FederationHost, error) {
+func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forgefed.FederationHost, error) {
+ actionsUser := user.NewActionsUser()
+ clientFactory, err := activitypub.GetClientFactory(ctx)
+ if err != nil {
+ return nil, err
+ }
+ client, err := clientFactory.WithKeys(ctx, actionsUser, "no idea where to get key material.")
+ if err != nil {
+ return nil, err
+ }
+ body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
+ if err != nil {
+ return nil, err
+ }
+ nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body)
+ if err != nil {
+ return nil, err
+ }
+ body, err = client.GetBody(nodeInfoWellKnown.Href)
+ if err != nil {
+ return nil, err
+ }
+ nodeInfo, err := forgefed.NewNodeInfo(body)
+ if err != nil {
+ return nil, err
+ }
+ result, err := forgefed.NewFederationHost(nodeInfo, actorID.Host)
+ if err != nil {
+ return nil, err
+ }
+ err = forgefed.CreateFederationHost(ctx, &result)
+ if err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.FederationHost, error) {
+ log.Info("Input was: %v", actorURI)
rawActorID, err := fm.NewActorID(actorURI)
if err != nil {
return nil, err
}
- federationHost, err := forgefed.FindFederationHostByFqdnAndPort(ctx, rawActorID.Host, rawActorID.HostPort)
+ federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host)
if err != nil {
return nil, err
}
if federationHost == nil {
- result, err := createFederationHostFromAP(ctx, rawActorID)
+ result, err := CreateFederationHostFromAP(ctx, rawActorID)
if err != nil {
return nil, err
}
@@ -48,109 +154,19 @@ func FindOrCreateFederationHost(ctx context.Context, actorURI string) (*forgefed
return federationHost, nil
}
-func FindOrCreateFederatedUser(ctx context.Context, actorURI string) (*user.User, *user.FederatedUser, *forgefed.FederationHost, error) {
- user, federatedUser, federationHost, err := findFederatedUser(ctx, actorURI)
- if err != nil {
- return nil, nil, nil, err
- }
- personID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName))
- if err != nil {
- return nil, nil, nil, err
- }
-
- if user != nil {
- log.Trace("Local ActivityPub user found (actorURI: %#v, user: %#v)", actorURI, user)
- } else {
- log.Trace("Attempting to create new user and federatedUser for actorURI: %#v", actorURI)
- user, federatedUser, err = createUserFromAP(ctx, personID, federationHost.ID)
- if err != nil {
- return nil, nil, nil, err
- }
- log.Trace("Created user %#v with federatedUser %#v from distant server", user, federatedUser)
- }
- log.Trace("Got user: %v", user.Name)
-
- return user, federatedUser, federationHost, nil
-}
-
-func findFederatedUser(ctx context.Context, actorURI string) (*user.User, *user.FederatedUser, *forgefed.FederationHost, error) {
- federationHost, err := FindOrCreateFederationHost(ctx, actorURI)
- if err != nil {
- return nil, nil, nil, err
- }
- actorID, err := fm.NewPersonID(actorURI, string(federationHost.NodeInfo.SoftwareName))
- if err != nil {
- return nil, nil, nil, err
- }
-
- user, federatedUser, err := user.FindFederatedUser(ctx, actorID.ID, federationHost.ID)
- if err != nil {
- return nil, nil, nil, err
- }
-
- return user, federatedUser, federationHost, nil
-}
-
-func createFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forgefed.FederationHost, error) {
- actionsUser := user.NewAPServerActor()
-
- clientFactory, err := activitypub.GetClientFactory(ctx)
- if err != nil {
- return nil, err
- }
-
- client, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
- if err != nil {
- return nil, err
- }
-
- body, err := client.GetBody(actorID.AsWellKnownNodeInfoURI())
- if err != nil {
- return nil, err
- }
-
- nodeInfoWellKnown, err := forgefed.NewNodeInfoWellKnown(body)
- if err != nil {
- return nil, err
- }
-
- body, err = client.GetBody(nodeInfoWellKnown.Href)
- if err != nil {
- return nil, err
- }
-
- nodeInfo, err := forgefed.NewNodeInfo(body)
- if err != nil {
- return nil, err
- }
-
- // TODO: we should get key material here also to have it immediately
- result, err := forgefed.NewFederationHost(actorID.Host, nodeInfo, actorID.HostPort, actorID.HostSchema)
- if err != nil {
- return nil, err
- }
-
- err = forgefed.CreateFederationHost(ctx, &result)
- if err != nil {
- return nil, err
- }
-
- return &result, nil
-}
-
-func fetchUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
- actionsUser := user.NewAPServerActor()
+func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
+ // ToDo: Do we get a publicKeyId from server, repo or owner or repo?
+ actionsUser := user.NewActionsUser()
clientFactory, err := activitypub.GetClientFactory(ctx)
if err != nil {
return nil, nil, err
}
-
- apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
+ client, err := clientFactory.WithKeys(ctx, actionsUser, "no idea where to get key material.")
if err != nil {
return nil, nil, err
}
- body, err := apClient.GetBody(personID.AsURI())
+ body, err := client.GetBody(personID.AsURI())
if err != nil {
return nil, nil, err
}
@@ -160,42 +176,26 @@ func fetchUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID
if err != nil {
return nil, nil, err
}
-
if res, err := validation.IsValid(person); !res {
return nil, nil, err
}
-
- log.Info("Fetched valid person from distant server: %q", person)
+ log.Info("Fetched valid person:%q", person)
localFqdn, err := url.ParseRequestURI(setting.AppURL)
if err != nil {
return nil, nil, err
}
-
email := fmt.Sprintf("f%v@%v", uuid.New().String(), localFqdn.Hostname())
loginName := personID.AsLoginName()
name := fmt.Sprintf("%v%v", person.PreferredUsername.String(), personID.HostSuffix())
fullName := person.Name.String()
-
if len(person.Name) == 0 {
fullName = name
}
-
password, err := password.Generate(32)
if err != nil {
return nil, nil, err
}
-
- inbox, err := url.ParseRequestURI(person.Inbox.GetLink().String())
- if err != nil {
- return nil, nil, err
- }
-
- pubKeyBytes, err := decodePublicKeyPem(person.PublicKey.PublicKeyPem)
- if err != nil {
- return nil, nil, err
- }
-
newUser := user.User{
LowerName: strings.ToLower(name),
Name: name,
@@ -207,37 +207,89 @@ func fetchUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID
LoginName: loginName,
Type: user.UserTypeRemoteUser,
IsAdmin: false,
+ NormalizedFederatedURI: personID.AsURI(),
}
-
federatedUser := user.FederatedUser{
- ExternalID: personID.ID,
- FederationHostID: federationHostID,
- InboxPath: inbox.Path,
- NormalizedOriginalURL: personID.AsURI(),
- KeyID: sql.NullString{
- String: person.PublicKey.ID.String(),
- Valid: true,
- },
- PublicKey: sql.Null[sql.RawBytes]{
- V: pubKeyBytes,
- Valid: true,
- },
+ ExternalID: personID.ID,
+ FederationHostID: federationHostID,
}
+ err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
+ if err != nil {
+ return nil, nil, err
+ }
+ log.Info("Created federatedUser:%q", federatedUser)
- log.Info("Fetched person's %q federatedUser from distant server: %q", person, federatedUser)
return &newUser, &federatedUser, nil
}
-func createUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) {
- newUser, federatedUser, err := fetchUserFromAP(ctx, personID, federationHostID)
- if err != nil {
- return nil, nil, err
- }
- err = user.CreateFederatedUser(ctx, newUser, federatedUser)
- if err != nil {
- return nil, nil, err
+// Create or update a list of FollowingRepo structs
+func StoreFollowingRepoList(ctx context.Context, localRepoID int64, followingRepoList []string) (int, string, error) {
+ followingRepos := make([]*repo.FollowingRepo, 0, len(followingRepoList))
+ for _, uri := range followingRepoList {
+ federationHost, err := GetFederationHostForURI(ctx, uri)
+ if err != nil {
+ return http.StatusInternalServerError, "Wrong FederationHost", err
+ }
+ followingRepoID, err := fm.NewRepositoryID(uri, string(federationHost.NodeInfo.SoftwareName))
+ if err != nil {
+ return http.StatusNotAcceptable, "Invalid federated repo", err
+ }
+ followingRepo, err := repo.NewFollowingRepo(localRepoID, followingRepoID.ID, federationHost.ID, uri)
+ if err != nil {
+ return http.StatusNotAcceptable, "Invalid federated repo", err
+ }
+ followingRepos = append(followingRepos, &followingRepo)
}
- log.Info("Created federatedUser: %q", federatedUser)
- return newUser, federatedUser, nil
+ if err := repo.StoreFollowingRepos(ctx, localRepoID, followingRepos); err != nil {
+ return 0, "", err
+ }
+
+ return 0, "", nil
+}
+
+func DeleteFollowingRepos(ctx context.Context, localRepoID int64) error {
+ return repo.StoreFollowingRepos(ctx, localRepoID, []*repo.FollowingRepo{})
+}
+
+func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error {
+ followingRepos, err := repo.FindFollowingReposByRepoID(ctx, repoID)
+ log.Info("Federated Repos is: %v", followingRepos)
+ if err != nil {
+ return err
+ }
+
+ likeActivityList := make([]fm.ForgeLike, 0)
+ for _, followingRepo := range followingRepos {
+ log.Info("Found following repo: %v", followingRepo)
+ target := followingRepo.URI
+ likeActivity, err := fm.NewForgeLike(doer.APActorID(), target, time.Now())
+ if err != nil {
+ return err
+ }
+ likeActivityList = append(likeActivityList, likeActivity)
+ }
+
+ apclientFactory, err := activitypub.GetClientFactory(ctx)
+ if err != nil {
+ return err
+ }
+ apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorID())
+ if err != nil {
+ return err
+ }
+ for i, activity := range likeActivityList {
+ activity.StartTime = activity.StartTime.Add(time.Duration(i) * time.Second)
+ json, err := activity.MarshalJSON()
+ if err != nil {
+ return err
+ }
+
+ _, err = apclient.Post(json, fmt.Sprintf("%v/inbox/", activity.Object))
+ if err != nil {
+ log.Error("error %v while sending activity: %q", err, activity)
+ }
+ }
+
+ return nil
}
diff --git a/services/federation/person_inbox_accept.go b/services/federation/person_inbox_accept.go
deleted file mode 100644
index d0a840bd2d..0000000000
--- a/services/federation/person_inbox_accept.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "net/http"
-
- "forgejo.org/modules/log"
-
- ap "github.com/go-ap/activitypub"
-)
-
-func processPersonInboxAccept(activity *ap.Activity) (ServiceResult, error) {
- if activity.Object.GetType() != ap.FollowType {
- log.Error("Invalid object type for Accept activity: %v", activity.Object.GetType())
- return ServiceResult{}, NewErrNotAcceptablef("invalid object type for Accept activity: %v", activity.Object.GetType())
- }
-
- // We currently do not do anything here, we just drop it.
- return NewServiceResultStatusOnly(http.StatusNoContent), nil
-}
diff --git a/services/federation/person_inbox_create.go b/services/federation/person_inbox_create.go
deleted file mode 100644
index 2132c7ede1..0000000000
--- a/services/federation/person_inbox_create.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
- "net/http"
-
- "forgejo.org/models/activities"
- "forgejo.org/models/user"
- fm "forgejo.org/modules/forgefed"
- "forgejo.org/modules/log"
-
- ap "github.com/go-ap/activitypub"
-)
-
-func processPersonInboxCreate(ctx context.Context, user *user.User, activity *ap.Activity) (ServiceResult, error) {
- createAct, err := fm.NewForgeUserActivityFromAp(*activity)
- if err != nil {
- log.Error("Invalid user activity: %v, %v", activity, err)
- return ServiceResult{}, NewErrNotAcceptablef("Invalid user activity: %v", err)
- }
-
- actorURI := createAct.Actor.GetLink().String()
- federatedBaseUser, _, _, err := findFederatedUser(ctx, actorURI)
- if err != nil {
- log.Error("Federated user not found (%s): %v", actorURI, err)
- return ServiceResult{}, NewErrNotAcceptablef("federated user not found (%s): %v", actorURI, err)
- }
- if federatedBaseUser == nil {
- log.Error("Federated user not found (%s): %v", actorURI, err)
- return ServiceResult{}, NewErrNotAcceptablef("federated user not found (%s): %v", actorURI, err)
- }
-
- federatedUserActivity, err := activities.NewFederatedUserActivity(
- user.ID,
- federatedBaseUser.ID,
- createAct.Actor.GetLink().String(),
- createAct.Note.Content.String(),
- createAct.Note.URL.GetID().String(),
- *activity,
- )
- if err != nil {
- log.Error("Error creating federatedUserActivity (%s): %v", actorURI, err)
- return ServiceResult{}, NewErrNotAcceptablef("Error creating federatedUserActivity: %v", err)
- }
-
- if err := activities.CreateUserActivity(ctx, &federatedUserActivity); err != nil {
- log.Error("Unable to record activity: %v", err)
- return ServiceResult{}, NewErrNotAcceptablef("Unable to record activity: %v", err)
- }
-
- return NewServiceResultStatusOnly(http.StatusNoContent), nil
-}
diff --git a/services/federation/person_inbox_follow.go b/services/federation/person_inbox_follow.go
deleted file mode 100644
index baa7934ad5..0000000000
--- a/services/federation/person_inbox_follow.go
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
- "fmt"
- "net/http"
-
- "forgejo.org/models/user"
- "forgejo.org/modules/forgefed"
- "forgejo.org/modules/log"
-
- ap "github.com/go-ap/activitypub"
- "github.com/go-ap/jsonld"
-)
-
-func processPersonFollow(ctx context.Context, ctxUser *user.User, activity *ap.Activity) (ServiceResult, error) {
- follow, err := forgefed.NewForgeFollowFromAp(*activity)
- if err != nil {
- log.Error("Invalid follow activity: %s", err)
- return ServiceResult{}, NewErrNotAcceptablef("Invalid follow activity: %v", err)
- }
-
- actorURI := follow.Actor.GetLink().String()
- _, federatedUser, federationHost, err := FindOrCreateFederatedUser(ctx, actorURI)
- if err != nil {
- log.Error("Error finding or creating federated user (%s): %v", actorURI, err)
- return ServiceResult{}, NewErrNotAcceptablef("Federated user not found: %v", err)
- }
-
- following, err := user.IsFollowingAp(ctx, ctxUser, federatedUser)
- if err != nil {
- log.Error("forgefed.IsFollowing: %v", err)
- return ServiceResult{}, NewErrNotAcceptablef("forgefed.IsFollowing: %v", err)
- }
- if following {
- // If the user is already following, we're good, nothing to do.
- log.Trace("Local user[%d] is already following federated user[%d]", ctxUser.ID, federatedUser.ID)
- return NewServiceResultStatusOnly(http.StatusNoContent), nil
- }
-
- follower, err := user.AddFollower(ctx, ctxUser, federatedUser)
- if err != nil {
- log.Error("Unable to add follower: %v", err)
- return ServiceResult{}, NewErrNotAcceptablef("Unable to add follower: %v", err)
- }
-
- accept := ap.AcceptNew(ap.IRI(fmt.Sprintf(
- "%s#accepts/follow/%d", ctxUser.APActorID(), follower.ID,
- )), follow)
- accept.Actor = ap.IRI(ctxUser.APActorID())
- payload, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI)).Marshal(accept)
- if err != nil {
- log.Error("Unable to Marshal JSON: %v", err)
- return ServiceResult{}, NewErrInternalf("MarshalJSON: %v", err)
- }
-
- hostURL := federationHost.AsURL()
- if err := deliveryQueue.Push(deliveryQueueItem{
- InboxURL: hostURL.JoinPath(federatedUser.InboxPath).String(),
- Doer: ctxUser,
- Payload: payload,
- }); err != nil {
- log.Error("Unable to push to pending queue: %v", err)
- return ServiceResult{}, NewErrInternalf("Unable to push to pending queue: %v", err)
- }
-
- // Respond back with an accept
- result := NewServiceResultWithBytes(http.StatusAccepted, []byte(`{"status":"Accepted"}`))
- return result, nil
-}
diff --git a/services/federation/person_inbox_undo.go b/services/federation/person_inbox_undo.go
deleted file mode 100644
index 4379cf242a..0000000000
--- a/services/federation/person_inbox_undo.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
- "net/http"
-
- "forgejo.org/models/user"
- "forgejo.org/modules/log"
-
- ap "github.com/go-ap/activitypub"
-)
-
-func processPersonInboxUndo(ctx context.Context, ctxUser *user.User, activity *ap.Activity) (ServiceResult, error) {
- if activity.Object.GetType() != ap.FollowType {
- log.Error("Invalid object type for Undo activity: %v", activity.Object.GetType())
- return ServiceResult{}, NewErrNotAcceptablef("Invalid object type for Undo activity: %v", activity.Object.GetType())
- }
-
- actorURI := activity.Actor.GetLink().String()
- _, federatedUser, _, err := findFederatedUser(ctx, actorURI)
- if err != nil {
- log.Error("User not found: %v", err)
- return ServiceResult{}, NewErrInternalf("User not found: %v", err)
- }
-
- if federatedUser != nil {
- following, err := user.IsFollowingAp(ctx, ctxUser, federatedUser)
- if err != nil {
- log.Error("forgefed.IsFollowing: %v", err)
- return ServiceResult{}, NewErrInternalf("forgefed.IsFollowing: %v", err)
- }
- if !following {
- // The local user is not following the federated one, nothing to do.
- log.Trace("Local user[%d] is not following federated user[%d]", ctxUser.ID, federatedUser.ID)
- return NewServiceResultStatusOnly(http.StatusNoContent), nil
- }
- if err := user.RemoveFollower(ctx, ctxUser, federatedUser); err != nil {
- log.Error("Unable to remove follower", err)
- return ServiceResult{}, NewErrInternalf("Unable to remove follower: %v", err)
- }
- }
-
- return NewServiceResultStatusOnly(http.StatusNoContent), nil
-}
diff --git a/services/federation/person_service.go b/services/federation/person_service.go
deleted file mode 100644
index d6482d013c..0000000000
--- a/services/federation/person_service.go
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
- "net/http"
-
- "forgejo.org/models/user"
- "forgejo.org/modules/forgefed"
- "forgejo.org/modules/log"
- context_service "forgejo.org/services/context"
-
- ap "github.com/go-ap/activitypub"
- "github.com/go-ap/jsonld"
-)
-
-func ProcessPersonInbox(ctx context.Context, user *user.User, activity *ap.Activity) (ServiceResult, error) {
- switch activity.Type {
- case ap.CreateType:
- return processPersonInboxCreate(ctx, user, activity)
- case ap.FollowType:
- return processPersonFollow(ctx, user, activity)
- case ap.UndoType:
- return processPersonInboxUndo(ctx, user, activity)
- case ap.AcceptType:
- return processPersonInboxAccept(activity)
- }
-
- log.Error("Unsupported PersonInbox activity: %v", activity.Type)
- return ServiceResult{}, NewErrNotAcceptablef("unsupported activity: %v", activity.Type)
-}
-
-func FollowRemoteActor(ctx *context_service.APIContext, localUser *user.User, actorURI string) error {
- _, federatedUser, federationHost, err := FindOrCreateFederatedUser(ctx.Base, actorURI)
- if err != nil {
- log.Error("Federated user not found (%s): %v", actorURI, err)
- ctx.Error(http.StatusNotAcceptable, "Federated user not found", err)
- return err
- }
-
- followReq, err := forgefed.NewForgeFollow(localUser.APActorID(), actorURI)
- if err != nil {
- return err
- }
-
- payload, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI)).
- Marshal(followReq)
- if err != nil {
- return err
- }
-
- hostURL := federationHost.AsURL()
- return deliveryQueue.Push(deliveryQueueItem{
- InboxURL: hostURL.JoinPath(federatedUser.InboxPath).String(),
- Doer: localUser,
- Payload: payload,
- })
-}
diff --git a/services/federation/repository_inbox_like.go b/services/federation/repository_inbox_like.go
deleted file mode 100644
index 478a12d92c..0000000000
--- a/services/federation/repository_inbox_like.go
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
- "fmt"
- "net/http"
- "time"
-
- "forgejo.org/models/forgefed"
- "forgejo.org/models/repo"
- "forgejo.org/models/user"
- "forgejo.org/modules/activitypub"
- fm "forgejo.org/modules/forgefed"
- "forgejo.org/modules/log"
- "forgejo.org/modules/validation"
- context_service "forgejo.org/services/context"
-
- ap "github.com/go-ap/activitypub"
-)
-
-// ProcessLikeActivity receives a ForgeLike activity and does the following:
-// Validation of the activity
-// Creation of a (remote) federationHost if not existing
-// Creation of a forgefed Person if not existing
-// Validation of incoming RepositoryID against Local RepositoryID
-// Star the repo if it wasn't already stared
-// Do some mitigation against out of order attacks
-func ProcessLikeActivity(ctx context.Context, activity *ap.Activity, repositoryID int64) (ServiceResult, error) {
- constructorLikeActivity, _ := fm.NewForgeLike(activity.Actor.GetLink().String(), activity.Object.GetLink().String(), activity.StartTime)
- if res, err := validation.IsValid(constructorLikeActivity); !res {
- return ServiceResult{}, NewErrNotAcceptablef("Invalid activity: %v", err)
- }
- log.Trace("Activity validated: %#v", activity)
-
- // parse actorID (person)
- actorURI := activity.Actor.GetID().String()
- user, _, federationHost, err := FindOrCreateFederatedUser(ctx, actorURI)
- if err != nil {
- log.Error("Federated user not found (%s): %v", actorURI, err)
- return ServiceResult{}, NewErrNotAcceptablef("FindOrCreateFederatedUser failed: %v", err)
- }
-
- if !constructorLikeActivity.IsNewer(federationHost.LatestActivity) {
- return ServiceResult{}, NewErrNotAcceptablef("LatestActivity: activity already processed: %v", err)
- }
-
- // parse objectID (repository)
- objectID, err := fm.NewRepositoryID(constructorLikeActivity.Object.GetID().String(), string(forgefed.ForgejoSourceType))
- if err != nil {
- return ServiceResult{}, NewErrNotAcceptablef("Parsing repo objectID failed: %v", err)
- }
- if objectID.ID != fmt.Sprint(repositoryID) {
- return ServiceResult{}, NewErrNotAcceptablef("Invalid repoId: %v", err)
- }
- log.Trace("Object accepted: %#v", objectID)
-
- // execute the activity if the repo was not stared already
- alreadyStared := repo.IsStaring(ctx, user.ID, repositoryID)
- if !alreadyStared {
- err = repo.StarRepo(ctx, user.ID, repositoryID, true)
- if err != nil {
- return ServiceResult{}, NewErrNotAcceptablef("Staring failed: %v", err)
- }
- }
- federationHost.LatestActivity = activity.StartTime
- err = forgefed.UpdateFederationHost(ctx, federationHost)
- if err != nil {
- return ServiceResult{}, NewErrNotAcceptablef("Updating federatedHost failed: %v", err)
- }
-
- return NewServiceResultStatusOnly(http.StatusNoContent), nil
-}
-
-// Create or update a list of FollowingRepo structs
-func StoreFollowingRepoList(ctx *context_service.Context, localRepoID int64, followingRepoList []string) (int, string, error) {
- followingRepos := make([]*repo.FollowingRepo, 0, len(followingRepoList))
- for _, uri := range followingRepoList {
- federationHost, err := FindOrCreateFederationHost(ctx.Base, uri)
- if err != nil {
- return http.StatusInternalServerError, "Wrong FederationHost", err
- }
- followingRepoID, err := fm.NewRepositoryID(uri, string(federationHost.NodeInfo.SoftwareName))
- if err != nil {
- return http.StatusNotAcceptable, "Invalid federated repo", err
- }
- followingRepo, err := repo.NewFollowingRepo(localRepoID, followingRepoID.ID, federationHost.ID, uri)
- if err != nil {
- return http.StatusNotAcceptable, "Invalid federated repo", err
- }
- followingRepos = append(followingRepos, &followingRepo)
- }
-
- if err := repo.StoreFollowingRepos(ctx, localRepoID, followingRepos); err != nil {
- return 0, "", err
- }
-
- return 0, "", nil
-}
-
-func DeleteFollowingRepos(ctx context.Context, localRepoID int64) error {
- return repo.StoreFollowingRepos(ctx, localRepoID, []*repo.FollowingRepo{})
-}
-
-func SendLikeActivities(ctx context.Context, doer user.User, repoID int64) error {
- followingRepos, err := repo.FindFollowingReposByRepoID(ctx, repoID)
- log.Trace("Federated Repos is: %#v", followingRepos)
- if err != nil {
- return err
- }
-
- likeActivityList := make([]fm.ForgeLike, 0)
- for _, followingRepo := range followingRepos {
- log.Trace("Found following repo: %#v", followingRepo)
- target := followingRepo.URI
- likeActivity, err := fm.NewForgeLike(doer.APActorID(), target, time.Now())
- if err != nil {
- return err
- }
- likeActivityList = append(likeActivityList, likeActivity)
- }
-
- apclientFactory, err := activitypub.GetClientFactory(ctx)
- if err != nil {
- return err
- }
- apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorID()+"#main-key")
- if err != nil {
- return err
- }
- for i, activity := range likeActivityList {
- activity.StartTime = activity.StartTime.Add(time.Duration(i) * time.Second)
- json, err := activity.MarshalJSON()
- if err != nil {
- return err
- }
-
- _, err = apclient.Post(json, fmt.Sprintf("%v/inbox", activity.Object))
- if err != nil {
- log.Error("error %v while sending activity: %#v", err, activity)
- }
- }
-
- return nil
-}
diff --git a/services/federation/repository_service.go b/services/federation/repository_service.go
deleted file mode 100644
index 7891d786e2..0000000000
--- a/services/federation/repository_service.go
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
-
- ap "github.com/go-ap/activitypub"
-)
-
-func ProcessRepositoryInbox(ctx context.Context, activity *ap.Activity, repositoryID int64) (ServiceResult, error) {
- switch activity.Type {
- case ap.LikeType:
- return ProcessLikeActivity(ctx, activity, repositoryID)
- default:
- return ServiceResult{}, NewErrNotAcceptablef("Not a like activity: %v", activity.Type)
- }
-}
diff --git a/services/federation/result.go b/services/federation/result.go
deleted file mode 100644
index 47afb2bdf6..0000000000
--- a/services/federation/result.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import "github.com/go-ap/activitypub"
-
-type ServiceResult struct {
- HTTPStatus int
- Bytes []byte
- Activity activitypub.Activity
- withBytes bool
- withActivity bool
- statusOnly bool
-}
-
-func NewServiceResultStatusOnly(status int) ServiceResult {
- return ServiceResult{HTTPStatus: status, statusOnly: true}
-}
-
-func NewServiceResultWithBytes(status int, bytes []byte) ServiceResult {
- return ServiceResult{HTTPStatus: status, Bytes: bytes, withBytes: true}
-}
-
-func (serviceResult ServiceResult) WithBytes() bool {
- return serviceResult.withBytes
-}
-
-func (serviceResult ServiceResult) WithActivity() bool {
- return serviceResult.withActivity
-}
-
-func (serviceResult ServiceResult) StatusOnly() bool {
- return serviceResult.statusOnly
-}
diff --git a/services/federation/signature_service.go b/services/federation/signature_service.go
deleted file mode 100644
index fd8cbb39cd..0000000000
--- a/services/federation/signature_service.go
+++ /dev/null
@@ -1,235 +0,0 @@
-// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
- "crypto/x509"
- "database/sql"
- "encoding/pem"
- "errors"
- "fmt"
- "net/url"
-
- "forgejo.org/models/forgefed"
- "forgejo.org/models/user"
- "forgejo.org/modules/activitypub"
- fm "forgejo.org/modules/forgefed"
-
- ap "github.com/go-ap/activitypub"
-)
-
-// Factory function for ActorID. Created struct is asserted to be valid
-func NewActorIDFromKeyID(ctx context.Context, uri string) (fm.ActorID, error) {
- parsedURI, err := url.Parse(uri)
- parsedURI.Fragment = ""
- if err != nil {
- return fm.ActorID{}, err
- }
-
- actionsUser := user.NewAPServerActor()
- clientFactory, err := activitypub.GetClientFactory(ctx)
- if err != nil {
- return fm.ActorID{}, err
- }
-
- apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
- if err != nil {
- return fm.ActorID{}, err
- }
-
- userResponse, err := apClient.GetBody(parsedURI.String())
- if err != nil {
- return fm.ActorID{}, err
- }
-
- var actor ap.Actor
- err = actor.UnmarshalJSON(userResponse)
- if err != nil {
- return fm.ActorID{}, err
- }
-
- result, err := fm.NewActorID(actor.PublicKey.Owner.String())
- return result, err
-}
-
-func FindOrCreateFederatedUserKey(ctx context.Context, keyID string) (pubKey any, err error) {
- var federatedUser *user.FederatedUser
- var keyURL *url.URL
-
- keyURL, err = url.Parse(keyID)
- if err != nil {
- return nil, err
- }
-
- // Try if the signing actor is an already known federated user
- _, federatedUser, err = user.FindFederatedUserByKeyID(ctx, keyURL.String())
- if err != nil {
- return nil, err
- }
-
- if federatedUser == nil {
- rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
- if err != nil {
- return nil, err
- }
-
- _, federatedUser, _, err = FindOrCreateFederatedUser(ctx, rawActorID.AsURI())
- if err != nil {
- return nil, err
- }
- } else {
- _, err = forgefed.GetFederationHost(ctx, federatedUser.FederationHostID)
- if err != nil {
- return nil, err
- }
- }
-
- if federatedUser.PublicKey.Valid {
- pubKey, err := x509.ParsePKIXPublicKey(federatedUser.PublicKey.V)
- if err != nil {
- return nil, err
- }
- return pubKey, nil
- }
-
- // Fetch missing public key
- pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
- if err != nil {
- return nil, err
- }
- if apPerson.Type == ap.ActivityVocabularyType("Person") {
- // Check federatedUser.id = person.id
- if federatedUser.ExternalID != apPerson.ID.String() {
- return nil, fmt.Errorf("federated user fetched (%v) does not match the stored one %v", apPerson, federatedUser)
- }
- // update federated user
- federatedUser.KeyID = sql.NullString{
- String: apPerson.PublicKey.ID.String(),
- Valid: true,
- }
- federatedUser.PublicKey = sql.Null[sql.RawBytes]{
- V: pubKeyBytes,
- Valid: true,
- }
- err = user.UpdateFederatedUser(ctx, federatedUser)
- if err != nil {
- return nil, err
- }
- return pubKey, nil
- }
- return nil, nil
-}
-
-func FindOrCreateFederationHostKey(ctx context.Context, keyID string) (pubKey any, err error) {
- keyURL, err := url.Parse(keyID)
- if err != nil {
- return nil, err
- }
- rawActorID, err := NewActorIDFromKeyID(ctx, keyID)
- if err != nil {
- return nil, err
- }
-
- // Is there an already known federation host?
- federationHost, err := forgefed.FindFederationHostByKeyID(ctx, keyURL.String())
- if err != nil {
- return nil, err
- }
-
- if federationHost == nil {
- federationHost, err = FindOrCreateFederationHost(ctx, rawActorID.AsURI())
- if err != nil {
- return nil, err
- }
- }
-
- // Is there an already an key?
- if federationHost.PublicKey.Valid {
- pubKey, err := x509.ParsePKIXPublicKey(federationHost.PublicKey.V)
- if err != nil {
- return nil, err
- }
- return pubKey, nil
- }
-
- // If not, fetch missing public key
- pubKey, pubKeyBytes, apPerson, err := fetchKeyFromAp(ctx, *keyURL)
- if err != nil {
- return nil, err
- }
- if apPerson.Type == ap.ActivityVocabularyType("Application") {
- // Check federationhost.id = person.id
- if federationHost.HostPort != rawActorID.HostPort || federationHost.HostFqdn != rawActorID.Host ||
- federationHost.HostSchema != rawActorID.HostSchema {
- return nil, fmt.Errorf("federation host fetched (%v) does not match the stored one %v", apPerson, federationHost)
- }
- // update federation host
- federationHost.KeyID = sql.NullString{
- String: apPerson.PublicKey.ID.String(),
- Valid: true,
- }
- federationHost.PublicKey = sql.Null[sql.RawBytes]{
- V: pubKeyBytes,
- Valid: true,
- }
- err = forgefed.UpdateFederationHost(ctx, federationHost)
- if err != nil {
- return nil, err
- }
- return pubKey, nil
- }
- return nil, nil
-}
-
-func fetchKeyFromAp(ctx context.Context, keyURL url.URL) (pubKey any, pubKeyBytes []byte, apPerson *ap.Person, err error) {
- actionsUser := user.NewAPServerActor()
-
- clientFactory, err := activitypub.GetClientFactory(ctx)
- if err != nil {
- return nil, nil, nil, err
- }
-
- apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
- if err != nil {
- return nil, nil, nil, err
- }
-
- b, err := apClient.GetBody(keyURL.String())
- if err != nil {
- return nil, nil, nil, err
- }
-
- person := ap.PersonNew(ap.IRI(keyURL.String()))
- err = person.UnmarshalJSON(b)
- if err != nil {
- return nil, nil, nil, fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %w", err)
- }
-
- pubKeyFromAp := person.PublicKey
- if pubKeyFromAp.ID.String() != keyURL.String() {
- return nil, nil, nil, fmt.Errorf("cannot find publicKey with id: %v in %v", keyURL, string(b))
- }
-
- pubKeyBytes, err = decodePublicKeyPem(pubKeyFromAp.PublicKeyPem)
- if err != nil {
- return nil, nil, nil, err
- }
-
- pubKey, err = x509.ParsePKIXPublicKey(pubKeyBytes)
- if err != nil {
- return nil, nil, nil, err
- }
-
- return pubKey, pubKeyBytes, person, err
-}
-
-func decodePublicKeyPem(pubKeyPem string) ([]byte, error) {
- block, _ := pem.Decode([]byte(pubKeyPem))
- if block == nil || block.Type != "PUBLIC KEY" {
- return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
- }
-
- return block.Bytes, nil
-}
diff --git a/services/federation/user_activity.go b/services/federation/user_activity.go
deleted file mode 100644
index 0db2aee4ec..0000000000
--- a/services/federation/user_activity.go
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package federation
-
-import (
- "context"
-
- activities_model "forgejo.org/models/activities"
- "forgejo.org/models/forgefed"
- "forgejo.org/models/user"
- "forgejo.org/modules/setting"
- "forgejo.org/modules/structs"
- "forgejo.org/services/convert"
-
- ap "github.com/go-ap/activitypub"
- "github.com/go-ap/jsonld"
-)
-
-func SendUserActivity(ctx context.Context, doer *user.User, activity *activities_model.Action) error {
- followers, err := user.GetFollowersForUser(ctx, doer)
- if err != nil {
- return err
- }
-
- userActivity, err := convert.ActionToForgeUserActivity(ctx, activity)
- if err != nil {
- return err
- }
-
- payload, err := jsonld.WithContext(
- jsonld.IRI(ap.ActivityBaseURI),
- ).Marshal(userActivity)
- if err != nil {
- return err
- }
-
- for _, follower := range followers {
- _, federatedUserFollower, err := user.GetFederatedUserByUserID(ctx, follower.FollowingUserID)
- if err != nil {
- return err
- }
-
- federationHost, err := forgefed.GetFederationHost(ctx, federatedUserFollower.FederationHostID)
- if err != nil {
- return err
- }
-
- hostURL := federationHost.AsURL()
- if err := deliveryQueue.Push(deliveryQueueItem{
- InboxURL: hostURL.JoinPath(federatedUserFollower.InboxPath).String(),
- Doer: doer,
- Payload: payload,
- }); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func NotifyActivityPubFollowers(ctx context.Context, actions []activities_model.Action) error {
- if !setting.Federation.Enabled {
- return nil
- }
- for _, act := range actions {
- if act.Repo != nil {
- if act.Repo.IsPrivate {
- continue
- }
- if act.Repo.Owner.KeepActivityPrivate || act.Repo.Owner.Visibility != structs.VisibleTypePublic {
- continue
- }
- }
- if act.ActUser.KeepActivityPrivate || act.ActUser.Visibility != structs.VisibleTypePublic {
- continue
- }
- if err := SendUserActivity(ctx, act.ActUser, &act); err != nil {
- return err
- }
- }
- return nil
-}
diff --git a/services/feed/action.go b/services/feed/action.go
index 96de080691..a2cd0551a3 100644
--- a/services/feed/action.go
+++ b/services/feed/action.go
@@ -19,7 +19,6 @@ import (
"forgejo.org/modules/repository"
"forgejo.org/modules/setting"
"forgejo.org/modules/util"
- federation_service "forgejo.org/services/federation"
notify_service "forgejo.org/services/notify"
)
@@ -40,22 +39,6 @@ func NewNotifier() notify_service.Notifier {
return &actionNotifier{}
}
-func notifyAll(ctx context.Context, action *activities_model.Action) error {
- out, err := activities_model.NotifyWatchers(ctx, action)
- if err != nil {
- return err
- }
- return federation_service.NotifyActivityPubFollowers(ctx, out)
-}
-
-func notifyAllActions(ctx context.Context, acts []*activities_model.Action) error {
- out, err := activities_model.NotifyWatchersActions(ctx, acts)
- if err != nil {
- return err
- }
- return federation_service.NotifyActivityPubFollowers(ctx, out)
-}
-
func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User) {
if err := issue.LoadPoster(ctx); err != nil {
log.Error("issue.LoadPoster: %v", err)
@@ -67,11 +50,11 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue
}
repo := issue.Repo
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: issue.Poster.ID,
ActUser: issue.Poster,
OpType: activities_model.ActionCreateIssue,
- Content: encodeContent(fmt.Sprintf("%d", issue.Index), issue.Title),
+ Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
@@ -87,7 +70,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model
act := &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
- Content: encodeContent(fmt.Sprintf("%d", issue.Index), ""),
+ Content: fmt.Sprintf("%d|%s", issue.Index, ""),
RepoID: issue.Repo.ID,
Repo: issue.Repo,
Comment: actionComment,
@@ -108,7 +91,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model
}
// Notify watchers for whatever action comes in, ignore if no action type.
- if err := notifyAll(ctx, act); err != nil {
+ if err := activities_model.NotifyWatchers(ctx, act); err != nil {
log.Error("NotifyWatchers: %v", err)
}
}
@@ -125,9 +108,18 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode
Comment: comment,
CommentID: comment.ID,
IsPrivate: issue.Repo.IsPrivate,
- Content: encodeContent(fmt.Sprintf("%d", issue.Index), abbreviatedComment(comment.Content)),
}
+ truncatedContent, truncatedRight := util.SplitStringAtByteN(comment.Content, 200)
+ if truncatedRight != "" {
+ // in case the content is in a Latin family language, we remove the last broken word.
+ lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
+ if lastSpaceIdx != -1 && (len(truncatedContent)-lastSpaceIdx < 15) {
+ truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
+ }
+ }
+ act.Content = fmt.Sprintf("%d|%s", issue.Index, truncatedContent)
+
if issue.IsPull {
act.OpType = activities_model.ActionCommentPull
} else {
@@ -135,7 +127,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode
}
// Notify watchers for whatever action comes in, ignore if no action type.
- if err := notifyAll(ctx, act); err != nil {
+ if err := activities_model.NotifyWatchers(ctx, act); err != nil {
log.Error("NotifyWatchers: %v", err)
}
}
@@ -154,11 +146,11 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model.
return
}
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: pull.Issue.Poster.ID,
ActUser: pull.Issue.Poster,
OpType: activities_model.ActionCreatePullRequest,
- Content: encodeContent(fmt.Sprintf("%d", pull.Issue.Index), pull.Issue.Title),
+ Content: fmt.Sprintf("%d|%s", pull.Issue.Index, pull.Issue.Title),
RepoID: pull.Issue.Repo.ID,
Repo: pull.Issue.Repo,
IsPrivate: pull.Issue.Repo.IsPrivate,
@@ -168,7 +160,7 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model.
}
func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionRenameRepo,
@@ -182,7 +174,7 @@ func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.
}
func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionTransferRepo,
@@ -196,7 +188,7 @@ func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_mode
}
func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionCreateRepo,
@@ -209,7 +201,7 @@ func (a *actionNotifier) CreateRepository(ctx context.Context, doer, u *user_mod
}
func (a *actionNotifier) ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionCreateRepo,
@@ -238,7 +230,7 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model
actions = append(actions, &activities_model.Action{
ActUserID: review.Reviewer.ID,
ActUser: review.Reviewer,
- Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), abbreviatedComment(comm.Content)),
+ Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comm.Content, "\n")[0]),
OpType: activities_model.ActionCommentPull,
RepoID: review.Issue.RepoID,
Repo: review.Issue.Repo,
@@ -254,7 +246,7 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model
action := &activities_model.Action{
ActUserID: review.Reviewer.ID,
ActUser: review.Reviewer,
- Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), abbreviatedComment(comment.Content)),
+ Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comment.Content, "\n")[0]),
RepoID: review.Issue.RepoID,
Repo: review.Issue.Repo,
IsPrivate: review.Issue.Repo.IsPrivate,
@@ -274,17 +266,17 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model
actions = append(actions, action)
}
- if err := notifyAllActions(ctx, actions); err != nil {
+ if err := activities_model.NotifyWatchersActions(ctx, actions); err != nil {
log.Error("notify watchers '%d/%d': %v", review.Reviewer.ID, review.Issue.RepoID, err)
}
}
func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionMergePullRequest,
- Content: encodeContent(fmt.Sprintf("%d", pr.Issue.Index), pr.Issue.Title),
+ Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
RepoID: pr.Issue.Repo.ID,
Repo: pr.Issue.Repo,
IsPrivate: pr.Issue.Repo.IsPrivate,
@@ -294,11 +286,11 @@ func (*actionNotifier) MergePullRequest(ctx context.Context, doer *user_model.Us
}
func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionAutoMergePullRequest,
- Content: encodeContent(fmt.Sprintf("%d", pr.Issue.Index), pr.Issue.Title),
+ Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
RepoID: pr.Issue.Repo.ID,
Repo: pr.Issue.Repo,
IsPrivate: pr.Issue.Repo.IsPrivate,
@@ -307,16 +299,16 @@ func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_mode
}
}
-func (*actionNotifier) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
+func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
reviewerName := review.Reviewer.Name
if len(review.OriginalAuthor) > 0 {
reviewerName = review.OriginalAuthor
}
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionPullReviewDismissed,
- Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), reviewerName, abbreviatedComment(comment.Content)),
+ Content: fmt.Sprintf("%d|%s|%s", review.Issue.Index, reviewerName, comment.Content),
RepoID: review.Issue.Repo.ID,
Repo: review.Issue.Repo,
IsPrivate: review.Issue.Repo.IsPrivate,
@@ -328,7 +320,9 @@ func (*actionNotifier) PullReviewDismiss(ctx context.Context, doer *user_model.U
}
func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
- commits = prepareCommitsForFeed(commits)
+ if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
+ commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
data, err := json.Marshal(commits)
if err != nil {
@@ -348,7 +342,7 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use
opType = activities_model.ActionDeleteBranch
}
- if err = notifyAll(ctx, &activities_model.Action{
+ if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: pusher.ID,
ActUser: pusher,
OpType: opType,
@@ -368,7 +362,7 @@ func (a *actionNotifier) CreateRef(ctx context.Context, doer *user_model.User, r
// has sent same action in `PushCommits`, so skip it.
return
}
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: opType,
@@ -387,7 +381,7 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r
// has sent same action in `PushCommits`, so skip it.
return
}
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: opType,
@@ -401,7 +395,9 @@ func (a *actionNotifier) DeleteRef(ctx context.Context, doer *user_model.User, r
}
func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
- commits = prepareCommitsForFeed(commits)
+ if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
+ commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
data, err := json.Marshal(commits)
if err != nil {
@@ -409,7 +405,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model
return
}
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: repo.OwnerID,
ActUser: repo.MustOwner(ctx),
OpType: activities_model.ActionMirrorSyncPush,
@@ -424,7 +420,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model
}
func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: repo.OwnerID,
ActUser: repo.MustOwner(ctx),
OpType: activities_model.ActionMirrorSyncCreate,
@@ -438,7 +434,7 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use
}
func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) {
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: repo.OwnerID,
ActUser: repo.MustOwner(ctx),
OpType: activities_model.ActionMirrorSyncDelete,
@@ -456,7 +452,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release
log.Error("LoadAttributes: %v", err)
return
}
- if err := notifyAll(ctx, &activities_model.Action{
+ if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
ActUserID: rel.PublisherID,
ActUser: rel.Publisher,
OpType: activities_model.ActionPublishRelease,
@@ -469,73 +465,3 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release
log.Error("NotifyWatchers: %v", err)
}
}
-
-// ... later decoded in models/activities/action.go:GetIssueInfos
-func encodeContent(params ...string) string {
- contentEncoded, err := json.Marshal(params)
- if err != nil {
- log.Error("encodeContent: Unexpected json encoding error: %v", err)
- }
- return string(contentEncoded)
-}
-
-// Given a comment of arbitrary-length Markdown text, create an abbreviated Markdown text appropriate for the
-// activity feed.
-func abbreviatedComment(comment string) string {
- firstLine := strings.Split(comment, "\n")[0]
-
- if strings.HasPrefix(firstLine, "```") {
- // First line is is a fenced code block... with no special abbreviate we would display a blank block, or in the
- // worst-case a ```mermaid would display an error. Better to omit the comment.
- return ""
- }
-
- truncatedContent, truncatedRight := util.SplitStringAtByteN(firstLine, 200)
- if truncatedRight != "" {
- // in case the content is in a Latin family language, we remove the last broken word.
- lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
- if lastSpaceIdx != -1 && (len(truncatedContent)-lastSpaceIdx < 15) {
- truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
- }
- }
-
- return truncatedContent
-}
-
-// Return a clone of the incoming repository.PushCommits that is appropriately tweaked for the activity feed. The struct
-// is cloned rather than modified in-place because the same data will be sent to multiple notifiers. Transformations
-// applied are: # of commits are limited to FeedMaxCommitNum, commit messages are trimmed to just the content displayed
-// in the activity feed.
-func prepareCommitsForFeed(commits *repository.PushCommits) *repository.PushCommits {
- numCommits := min(len(commits.Commits), setting.UI.FeedMaxCommitNum)
- retval := repository.PushCommits{
- Commits: make([]*repository.PushCommit, 0, numCommits),
- HeadCommit: nil,
- CompareURL: commits.CompareURL,
- Len: commits.Len,
- }
- if commits.HeadCommit != nil {
- retval.HeadCommit = prepareCommitForFeed(commits.HeadCommit)
- }
- for i, commit := range commits.Commits {
- if i == numCommits {
- break
- }
- retval.Commits = append(retval.Commits, prepareCommitForFeed(commit))
- }
- return &retval
-}
-
-func prepareCommitForFeed(commit *repository.PushCommit) *repository.PushCommit {
- return &repository.PushCommit{
- Sha1: commit.Sha1,
- Message: abbreviatedComment(commit.Message),
- AuthorEmail: commit.AuthorEmail,
- AuthorName: commit.AuthorName,
- CommitterEmail: commit.CommitterEmail,
- CommitterName: commit.CommitterName,
- Signature: commit.Signature,
- Verification: commit.Verification,
- Timestamp: commit.Timestamp,
- }
-}
diff --git a/services/feed/action_test.go b/services/feed/action_test.go
index fd27bf32a9..b0bbcdc3b6 100644
--- a/services/feed/action_test.go
+++ b/services/feed/action_test.go
@@ -1,5 +1,4 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
-// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package feed
@@ -67,7 +66,7 @@ func pushCommits() *repository.PushCommits {
CommitterName: "User2",
AuthorEmail: "user2@example.com",
AuthorName: "User2",
- Message: "not signed commit\nline two",
+ Message: "not signed commit",
},
{
Sha1: "27566bd",
@@ -83,10 +82,10 @@ func pushCommits() *repository.PushCommits {
CommitterName: "User2",
AuthorEmail: "user2@example.com",
AuthorName: "User2",
- Message: "good signed commit\nlong commit message\nwith lots of details\nabout how cool the implementation is",
+ Message: "good signed commit",
},
}
- pushCommits.HeadCommit = &repository.PushCommit{Sha1: "69554a6", Message: "not signed commit\nline two"}
+ pushCommits.HeadCommit = &repository.PushCommit{Sha1: "69554a6"}
return pushCommits
}
@@ -103,7 +102,7 @@ func TestSyncPushCommits(t *testing.T) {
NewNotifier().SyncPushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("master")}, pushCommits())
newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/master"}, unittest.Cond("id > ?", maxID))
- assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"27566bd","Message":"good signed commit (with not yet validated email)","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"5099b81","Message":"good signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
+ assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"27566bd","Message":"good signed commit (with not yet validated email)","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"5099b81","Message":"good signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
})
t.Run("Only one commit", func(t *testing.T) {
@@ -113,23 +112,7 @@ func TestSyncPushCommits(t *testing.T) {
NewNotifier().SyncPushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("main")}, pushCommits())
newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/main"}, unittest.Cond("id > ?", maxID))
- assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
- })
-
- t.Run("Does not mutate commits param", func(t *testing.T) {
- defer test.MockVariableValue(&setting.UI.FeedMaxCommitNum, 1)()
-
- commits := pushCommits()
-
- assert.Equal(t, "not signed commit\nline two", commits.HeadCommit.Message)
- assert.Equal(t, "good signed commit\nlong commit message\nwith lots of details\nabout how cool the implementation is", commits.Commits[2].Message)
-
- NewNotifier().SyncPushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("master")}, commits)
-
- // commits passed into SyncPushCommits may be passed into other notifiers, so checking that the struct wasn't
- // mutated by truncate of messages, or truncation to match FeedMaxCommitNum (Commits[2])...
- assert.Equal(t, "not signed commit\nline two", commits.HeadCommit.Message)
- assert.Equal(t, "good signed commit\nlong commit message\nwith lots of details\nabout how cool the implementation is", commits.Commits[2].Message)
+ assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
})
}
@@ -146,7 +129,7 @@ func TestPushCommits(t *testing.T) {
NewNotifier().PushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("master")}, pushCommits())
newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/master"}, unittest.Cond("id > ?", maxID))
- assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"27566bd","Message":"good signed commit (with not yet validated email)","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"5099b81","Message":"good signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
+ assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"27566bd","Message":"good signed commit (with not yet validated email)","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"},{"Sha1":"5099b81","Message":"good signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
})
t.Run("Only one commit", func(t *testing.T) {
@@ -156,83 +139,6 @@ func TestPushCommits(t *testing.T) {
NewNotifier().PushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("main")}, pushCommits())
newNotification := unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ActUserID: user.ID, RefName: "refs/heads/main"}, unittest.Cond("id > ?", maxID))
- assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Signature":null,"Verification":null,"Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
- })
-
- t.Run("Does not mutate commits param", func(t *testing.T) {
- defer test.MockVariableValue(&setting.UI.FeedMaxCommitNum, 1)()
-
- commits := pushCommits()
-
- assert.Equal(t, "not signed commit\nline two", commits.HeadCommit.Message)
- assert.Equal(t, "good signed commit\nlong commit message\nwith lots of details\nabout how cool the implementation is", commits.Commits[2].Message)
-
- NewNotifier().PushCommits(db.DefaultContext, user, repo, &repository.PushUpdateOptions{RefFullName: git.RefNameFromBranch("main")}, commits)
-
- // commits passed into SyncPushCommits may be passed into other notifiers, so checking that the struct wasn't
- // mutated by truncate of messages, or truncation to match FeedMaxCommitNum (Commits[2])...
- assert.Equal(t, "not signed commit\nline two", commits.HeadCommit.Message)
- assert.Equal(t, "good signed commit\nlong commit message\nwith lots of details\nabout how cool the implementation is", commits.Commits[2].Message)
+ assert.JSONEq(t, `{"Commits":[{"Sha1":"69554a6","Message":"not signed commit","AuthorEmail":"user2@example.com","AuthorName":"User2","CommitterEmail":"user2@example.com","CommitterName":"User2","Timestamp":"0001-01-01T00:00:00Z"}],"HeadCommit":{"Sha1":"69554a6","Message":"","AuthorEmail":"","AuthorName":"","CommitterEmail":"","CommitterName":"","Timestamp":"0001-01-01T00:00:00Z"},"CompareURL":"","Len":0}`, newNotification.Content)
})
}
-
-func TestAbbreviatedComment(t *testing.T) {
- tests := []struct {
- name string
- input string
- expected string
- }{
- {
- name: "short single line comment",
- input: "This is a short comment",
- expected: "This is a short comment",
- },
- {
- name: "empty comment",
- input: "",
- expected: "",
- },
- {
- name: "multiline comment - only first line",
- input: "First line of comment\nSecond line\nThird line",
- expected: "First line of comment",
- },
- {
- name: "before clip boundry",
- input: strings.Repeat("abc ", 50),
- expected: strings.Repeat("abc ", 50),
- },
- {
- name: "after clip boundry",
- input: strings.Repeat("abc ", 51),
- expected: "abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc…",
- },
- {
- name: "byte-split would land in middle of a rune",
- input: strings.Repeat("🎉", 200),
- expected: "🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉…",
- },
- {
- name: "mermaid block",
- input: "Interesting point, here's a digram with my thoughts:\n```mermaid\ngraph LR\n a -->|some text| b\n```",
- expected: "Interesting point, here's a digram with my thoughts:",
- },
- {
- name: "block start",
- input: "```\n# This file describes the expected reviewers for a PR based on the changed\n# files.\n```\n\nI think this comment is wrong...",
- expected: "",
- },
- {
- name: "labeled block start",
- input: "```mermaid\ngraph LR\n a -->|some text| b\n```",
- expected: "",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := abbreviatedComment(tt.input)
- assert.Equal(t, tt.expected, result, "abbreviatedComment(%q)", tt.input)
- })
- }
-}
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index b89e87f749..e665ca0d19 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -79,7 +79,6 @@ type AuthenticationForm struct {
SkipLocalTwoFA bool
GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
- AllowUsernameChange bool
}
// Validate validates fields
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 11aac1fd52..c39c6a7b36 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -105,9 +105,6 @@ func ParseRemoteAddr(remoteAddr, authUsername, authPassword string) (string, err
if err != nil {
return "", &models.ErrInvalidCloneAddr{IsURLError: true, Host: remoteAddr}
}
- if u.User != nil {
- return "", &models.ErrInvalidCloneAddr{Host: remoteAddr, HasCredentials: true}
- }
if len(authUsername)+len(authPassword) > 0 {
u.User = url.UserPassword(authUsername, authPassword)
}
@@ -144,7 +141,6 @@ type RepoSettingForm struct {
PushMirrorSyncOnCommit bool
PushMirrorInterval string
PushMirrorUseSSH bool
- PushMirrorBranchFilter string `binding:"MaxSize(2048)" preprocess:"TrimSpace"`
Private bool
Template bool
EnablePrune bool
@@ -281,9 +277,6 @@ type WebhookCoreForm struct {
Wiki bool
Repository bool
Package bool
- ActionFailure bool
- ActionRecover bool
- ActionSuccess bool
Active bool
BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string
@@ -732,8 +725,8 @@ func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) bi
// AddTimeManuallyForm form that adds spent time manually.
type AddTimeManuallyForm struct {
- Hours int `binding:"Range(0,1000)" locale:"repo.issues.add_time_hours"`
- Minutes int `binding:"Range(0,1000)" locale:"repo.issues.add_time_minutes"`
+ Hours int `binding:"Range(0,1000)"`
+ Minutes int `binding:"Range(0,1000)"`
}
// Validate validates the fields
diff --git a/services/forms/report_abuse.go b/services/forms/report_abuse.go
deleted file mode 100644
index 5e9d7dc45f..0000000000
--- a/services/forms/report_abuse.go
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package forms
-
-import (
- "net/http"
-
- "forgejo.org/models/moderation"
- "forgejo.org/modules/web/middleware"
- "forgejo.org/services/context"
-
- "code.forgejo.org/go-chi/binding"
-)
-
-// ReportAbuseForm is used to interact with the UI of the form that submits new abuse reports.
-type ReportAbuseForm struct {
- ContentID int64
- ContentType moderation.ReportedContentType
- AbuseCategory moderation.AbuseCategoryType `binding:"Required" locale:"moderation.abuse_category"`
- Remarks string `binding:"Required;MinSize(20);MaxSize(500)" preprocess:"TrimSpace" locale:"moderation.report_remarks"`
-}
-
-// Validate validates the fields of ReportAbuseForm.
-func (f *ReportAbuseForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
- ctx := context.GetValidateContext(req)
- return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
-}
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 8c95139e2c..dfd5b3da9b 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -109,7 +109,7 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding.
// The email is marked as allowed if it matches any of the
// domains in the whitelist or if it doesn't match any of
// domains in the blocklist, if any such list is not empty.
-func (f *RegisterForm) IsEmailDomainAllowed() (validEmail, ok bool) {
+func (f *RegisterForm) IsEmailDomainAllowed() bool {
return validation.IsEmailDomainAllowed(f.Email)
}
diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go
index 4ce63ab552..67fb64cabf 100644
--- a/services/forms/user_form_test.go
+++ b/services/forms/user_form_test.go
@@ -9,24 +9,31 @@ import (
auth_model "forgejo.org/models/auth"
"forgejo.org/modules/setting"
- "forgejo.org/modules/test"
"github.com/gobwas/glob"
"github.com/stretchr/testify/assert"
)
func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) {
- defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, nil)()
+ oldService := setting.Service
+ defer func() {
+ setting.Service = oldService
+ }()
+
+ setting.Service.EmailDomainAllowList = nil
form := RegisterForm{}
- emailValid, ok := form.IsEmailDomainAllowed()
- assert.False(t, emailValid)
- assert.False(t, ok)
+ assert.True(t, form.IsEmailDomainAllowed())
}
func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
- defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io")})()
+ oldService := setting.Service
+ defer func() {
+ setting.Service = oldService
+ }()
+
+ setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io")}
tt := []struct {
email string
@@ -38,13 +45,17 @@ func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
for _, v := range tt {
form := RegisterForm{Email: v.email}
- _, ok := form.IsEmailDomainAllowed()
- assert.False(t, ok)
+ assert.False(t, form.IsEmailDomainAllowed())
}
}
func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) {
- defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")})()
+ oldService := setting.Service
+ defer func() {
+ setting.Service = oldService
+ }()
+
+ setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")}
tt := []struct {
email string
@@ -62,13 +73,18 @@ func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) {
for _, v := range tt {
form := RegisterForm{Email: v.email}
- _, ok := form.IsEmailDomainAllowed()
- assert.Equal(t, v.valid, ok)
+ assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
}
}
func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
- defer test.MockVariableValue(&setting.Service.EmailDomainBlockList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")})()
+ oldService := setting.Service
+ defer func() {
+ setting.Service = oldService
+ }()
+
+ setting.Service.EmailDomainAllowList = nil
+ setting.Service.EmailDomainBlockList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")}
tt := []struct {
email string
@@ -76,6 +92,7 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
}{
{"security@gitea.io", false},
{"security@gitea.example", true},
+ {"invalid", true},
{"user@my.block", false},
{"user@my.block1", true},
@@ -84,8 +101,7 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
for _, v := range tt {
form := RegisterForm{Email: v.email}
- _, ok := form.IsEmailDomainAllowed()
- assert.Equal(t, v.valid, ok)
+ assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
}
}
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 2f3d289c80..2e1fecda2a 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -85,20 +85,11 @@ type DiffLine struct {
// DiffLineSectionInfo represents diff line section meta data
type DiffLineSectionInfo struct {
- Path string
-
- // Last(Left/Right)Idx do not directly relate to this diff section, but indicate the last line number in the
- // previous diff section. Set to 0 for the first diff section of a file, and 1 for the first line of code in the
- // file.
- LastLeftIdx int
- LastRightIdx int
-
- // (Left/Right)Idx are the first line number in this diff section
- LeftIdx int
- RightIdx int
-
- // Number of lines contained within each diff section. In the UI, these fields are set to 0 in cases where a
- // section is being used as a placeholder at the end of a diff to allow expansion into the remainder of the file.
+ Path string
+ LastLeftIdx int
+ LastRightIdx int
+ LeftIdx int
+ RightIdx int
LeftHunkSize int
RightHunkSize int
}
@@ -166,7 +157,7 @@ func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
}
if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 {
return DiffLineExpandUp
- } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx-1 > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {
+ } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {
return DiffLineExpandUpDown
} else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 {
return DiffLineExpandDown
@@ -427,7 +418,6 @@ func (diffFile *DiffFile) ShouldBeHidden() bool {
return diffFile.IsGenerated || diffFile.IsViewed
}
-//llu:returnsTrKey
func (diffFile *DiffFile) ModeTranslationKey(mode string) string {
switch mode {
case "040000":
@@ -450,29 +440,11 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int {
if err != nil {
return 0
}
- reader, err := blob.DataAsync()
+ lineCount, err := blob.GetBlobLineCount()
if err != nil {
return 0
}
- defer reader.Close()
- buf := make([]byte, 32*1024)
- count := 1
- lineSep := []byte{'\n'}
-
- c, err := reader.Read(buf)
- if c == 0 && err == io.EOF {
- return 0
- }
- for {
- count += bytes.Count(buf[:c], lineSep)
- switch {
- case err == io.EOF:
- return count
- case err != nil:
- return count
- }
- c, err = reader.Read(buf)
- }
+ return lineCount
}
// Diff represents a difference between two git trees.
@@ -1088,7 +1060,7 @@ func readFileName(rd *strings.Reader) (string, bool) {
_, _ = fmt.Fscanf(rd, "%s ", &name)
char, _ := rd.ReadByte()
_ = rd.UnreadByte()
- for char != 0 && char != '"' && char != 'b' {
+ for !(char == 0 || char == '"' || char == 'b') {
var suffix string
_, _ = fmt.Fscanf(rd, "%s ", &suffix)
name += " " + suffix
@@ -1116,63 +1088,62 @@ type DiffOptions struct {
FileOnly bool
}
-// GetDiffSimple builds a Diff between two commits of a repository.
+// GetDiff builds a Diff between two commits of a repository.
// Passing the empty string as beforeCommitID returns a diff from the parent commit.
-// The whitespaceBehavior is either an empty string or a git flag.
-func GetDiffSimple(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (diff *Diff, afterCommit *git.Commit, err error) {
- afterCommit, err = gitRepo.GetCommit(opts.AfterCommitID)
+// The whitespaceBehavior is either an empty string or a git flag
+func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
+ repoPath := gitRepo.Path
+
+ var beforeCommit *git.Commit
+ commit, err := gitRepo.GetCommit(opts.AfterCommitID)
if err != nil {
- return nil, nil, fmt.Errorf("unable to get the after commit %q: %w", opts.AfterCommitID, err)
+ return nil, err
}
cmdCtx, cmdCancel := context.WithCancel(ctx)
defer cmdCancel()
- cmdDiff := git.NewCommand(cmdCtx).
- AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
- AddArguments(opts.WhitespaceBehavior...)
-
+ cmdDiff := git.NewCommand(cmdCtx)
objectFormat, err := gitRepo.GetObjectFormat()
if err != nil {
- return nil, nil, fmt.Errorf("not able to determine the object format: %w", err)
+ return nil, err
}
- // If before commit is empty or the empty object and the after commit has no
- // parents, then use the empty tree as before commit.
- //
- // This is the case for a 'initial commit' of a Git tree, which obviously has
- // no parents.
- if (len(opts.BeforeCommitID) == 0 || opts.BeforeCommitID == objectFormat.EmptyObjectID().String()) && afterCommit.ParentCount() == 0 {
- // Reset before commit ID to indicate empty tree was used.
- opts.BeforeCommitID = ""
- // Add enpty tree as before commit.
- cmdDiff.AddDynamicArguments(objectFormat.EmptyTree().String())
+ if (len(opts.BeforeCommitID) == 0 || opts.BeforeCommitID == objectFormat.EmptyObjectID().String()) && commit.ParentCount() == 0 {
+ cmdDiff.AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
+ AddArguments(opts.WhitespaceBehavior...).
+ AddDynamicArguments(objectFormat.EmptyTree().String()).
+ AddDynamicArguments(opts.AfterCommitID)
} else {
- // If before commit ID is empty, use the first parent of the after commit.
- if len(opts.BeforeCommitID) == 0 {
- parentCommit, err := afterCommit.Parent(0)
+ actualBeforeCommitID := opts.BeforeCommitID
+ if len(actualBeforeCommitID) == 0 {
+ parentCommit, err := commit.Parent(0)
if err != nil {
- return nil, nil, fmt.Errorf("not able to get first parent of %q: %w", afterCommit.ID.String(), err)
+ return nil, err
}
- opts.BeforeCommitID = parentCommit.ID.String()
+ actualBeforeCommitID = parentCommit.ID.String()
}
- cmdDiff.AddDynamicArguments(opts.BeforeCommitID)
- }
+ cmdDiff.AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
+ AddArguments(opts.WhitespaceBehavior...).
+ AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID)
+ opts.BeforeCommitID = actualBeforeCommitID
- // Add the after commit to the diff command.
- cmdDiff.AddDynamicArguments(opts.AfterCommitID)
+ beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID)
+ if err != nil {
+ return nil, err
+ }
+ }
// In git 2.31, git diff learned --skip-to which we can use to shortcut skip to file
// so if we are using at least this version of git we don't have to tell ParsePatch to do
// the skipping for us
parsePatchSkipToFile := opts.SkipTo
- if opts.SkipTo != "" {
+ if opts.SkipTo != "" && git.CheckGitVersionAtLeast("2.31") == nil {
cmdDiff.AddOptionFormat("--skip-to=%s", opts.SkipTo)
parsePatchSkipToFile = ""
}
- // If we only want to diff for some files, add that as well.
cmdDiff.AddDashesAndList(files...)
reader, writer := io.Pipe()
@@ -1182,7 +1153,6 @@ func GetDiffSimple(ctx context.Context, gitRepo *git.Repository, opts *DiffOptio
}()
go func() {
- repoPath := gitRepo.Path
stderr := &bytes.Buffer{}
cmdDiff.SetDescription(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath))
if err := cmdDiff.Run(&git.RunOpts{
@@ -1197,33 +1167,14 @@ func GetDiffSimple(ctx context.Context, gitRepo *git.Repository, opts *DiffOptio
_ = writer.Close()
}()
- diff, err = ParsePatch(cmdCtx, opts.MaxLines, opts.MaxLineCharacters, opts.MaxFiles, reader, parsePatchSkipToFile)
+ diff, err := ParsePatch(cmdCtx, opts.MaxLines, opts.MaxLineCharacters, opts.MaxFiles, reader, parsePatchSkipToFile)
// Ensure the git process is killed if it didn't exit already
cmdCancel()
if err != nil {
- return nil, nil, fmt.Errorf("unable to parse a git diff: %w", err)
+ return nil, fmt.Errorf("unable to ParsePatch: %w", err)
}
diff.Start = opts.SkipTo
- return diff, afterCommit, nil
-}
-
-func GetDiffFull(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
- diff, afterCommit, err := GetDiffSimple(ctx, gitRepo, opts, files...)
- if err != nil {
- return nil, err
- }
-
- // If there's a before commit, then GetDiffSimple will set it, otherwise it
- // is empty.
- var beforeCommit *git.Commit
- if len(opts.BeforeCommitID) != 0 {
- beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID)
- if err != nil {
- return nil, fmt.Errorf("unable to get before commit %q: %w", opts.BeforeCommitID, err)
- }
- }
-
checker, err := gitRepo.GitAttributeChecker(opts.AfterCommitID, git.LinguistAttributes...)
if err != nil {
return nil, fmt.Errorf("unable to GitAttributeChecker: %w", err)
@@ -1259,7 +1210,7 @@ func GetDiffFull(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions
diffFile.IsGenerated = analyze.IsGenerated(diffFile.Name)
}
- tailSection := diffFile.GetTailSection(gitRepo, beforeCommit, afterCommit)
+ tailSection := diffFile.GetTailSection(gitRepo, beforeCommit, commit)
if tailSection != nil {
diffFile.Sections = append(diffFile.Sections, tailSection)
}
@@ -1321,7 +1272,7 @@ func GetPullDiffStats(gitRepo *git.Repository, opts *DiffOptions) (*PullDiffStat
// SyncAndGetUserSpecificDiff is like GetDiff, except that user specific data such as which files the given user has already viewed on the given PR will also be set
// Additionally, the database asynchronously is updated if files have changed since the last review
func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
- diff, err := GetDiffFull(ctx, gitRepo, opts, files...)
+ diff, err := GetDiff(ctx, gitRepo, opts, files...)
if err != nil {
return nil, err
}
diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go
index d4d1cd4460..532255fe84 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -635,7 +635,7 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
defer gitRepo.Close()
for _, behavior := range []git.TrustedCmdArgs{{"-w"}, {"--ignore-space-at-eol"}, {"-b"}, nil} {
- diffs, _, err := GetDiffSimple(db.DefaultContext, gitRepo,
+ diffs, err := GetDiff(db.DefaultContext, gitRepo,
&DiffOptions{
AfterCommitID: "bd7063cc7c04689c4d082183d32a604ed27a24f9",
BeforeCommitID: "559c156f8e0178b71cb44355428f24001b08fc68",
@@ -651,229 +651,6 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
}
}
-func TestGetDiffFull(t *testing.T) {
- gitRepo, err := git.OpenRepository(git.DefaultContext, "./../../modules/git/tests/repos/language_stats_repo")
- require.NoError(t, err)
-
- defer gitRepo.Close()
-
- t.Run("Initial commit", func(t *testing.T) {
- diff, err := GetDiffFull(db.DefaultContext, gitRepo,
- &DiffOptions{
- AfterCommitID: "8fee858da5796dfb37704761701bb8e800ad9ef3",
- MaxLines: setting.Git.MaxGitDiffLines,
- MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
- MaxFiles: setting.Git.MaxGitDiffFiles,
- })
- require.NoError(t, err)
-
- assert.Empty(t, diff.Start)
- assert.Empty(t, diff.End)
- assert.False(t, diff.IsIncomplete)
- assert.Equal(t, 5, diff.NumFiles)
- assert.Equal(t, 23, diff.TotalAddition)
- assert.Len(t, diff.Files, 5)
-
- assert.True(t, diff.Files[0].IsVendored)
- assert.Equal(t, ".gitattributes", diff.Files[0].Name)
- assert.Equal(t, "24139dae656713ba861751fb2c2ac38839349a7a", diff.Files[0].NameHash)
-
- assert.Equal(t, "Python", diff.Files[1].Language)
- assert.Equal(t, "i-am-a-python.p", diff.Files[1].Name)
- assert.Equal(t, "32154957b043de62cbcdbe254a53ec4c3e00c5a0", diff.Files[1].NameHash)
-
- assert.Equal(t, "java-hello/main.java", diff.Files[2].Name)
- assert.Equal(t, "ef9f6a406a4cde7bb5480ba7b027bdc8cd6fa11d", diff.Files[2].NameHash)
-
- assert.Equal(t, "main.vendor.java", diff.Files[3].Name)
- assert.Equal(t, "c94fd7272f109d4d21d6df2b637c864a5ab63f46", diff.Files[3].NameHash)
-
- assert.Equal(t, "python-hello/hello.py", diff.Files[4].Name)
- assert.Equal(t, "021705ba8b98778dc4e277d3a6ea1b8c6122a7f9", diff.Files[4].NameHash)
- })
-
- t.Run("Normal diff", func(t *testing.T) {
- diff, err := GetDiffFull(db.DefaultContext, gitRepo,
- &DiffOptions{
- AfterCommitID: "341fca5b5ea3de596dc483e54c2db28633cd2f97",
- BeforeCommitID: "8fee858da5796dfb37704761701bb8e800ad9ef3",
- MaxLines: setting.Git.MaxGitDiffLines,
- MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
- MaxFiles: setting.Git.MaxGitDiffFiles,
- })
- require.NoError(t, err)
-
- assert.Empty(t, diff.Start)
- assert.Empty(t, diff.End)
- assert.False(t, diff.IsIncomplete)
- assert.Equal(t, 1, diff.NumFiles)
- assert.Equal(t, 1, diff.TotalAddition)
- assert.Len(t, diff.Files, 1)
-
- assert.Equal(t, ".gitattributes", diff.Files[0].Name)
- assert.Equal(t, "24139dae656713ba861751fb2c2ac38839349a7a", diff.Files[0].NameHash)
- assert.Len(t, diff.Files[0].Sections, 2)
- assert.Equal(t, 4, diff.Files[0].Sections[1].Lines[0].SectionInfo.LeftIdx)
- })
-}
-
-func TestDiffLine_GetExpandDirection(t *testing.T) {
- tests := []struct {
- name string
- diffLine *DiffLine
- expectedResult DiffLineExpandDirection
- }{
- {
- name: "non-section line - no expansion",
- diffLine: &DiffLine{
- Type: DiffLineAdd,
- SectionInfo: &DiffLineSectionInfo{},
- },
- expectedResult: DiffLineExpandNone,
- },
- {
- name: "nil section info - no expansion",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: nil,
- },
- expectedResult: DiffLineExpandNone,
- },
- {
- name: "no lines between",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: &DiffLineSectionInfo{
- // Previous section of the diff displayed up to line 530...
- LastRightIdx: 530,
- LastLeftIdx: 530,
- // This section of the diff starts at line 531...
- RightIdx: 531,
- LeftIdx: 531,
- },
- },
- // There are zero lines between 530 and 531, so we should have nothing to expand.
- expectedResult: DiffLineExpandNone,
- },
- {
- name: "first diff section is the start of the file",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: &DiffLineSectionInfo{
- // Last[...]Idx is set to zero when it's the first section in the file (and not 1, which would be
- // the first section -is- the first line of the file).
- LastRightIdx: 0,
- LastLeftIdx: 0,
- // The diff section is showing line 1, the top of th efile.
- RightIdx: 1,
- LeftIdx: 1,
- },
- },
- // We're at the top of the file; no expansion.
- expectedResult: DiffLineExpandNone,
- },
- {
- name: "first diff section doesn't start at the top of the file",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: &DiffLineSectionInfo{
- // Last[...]Idx is set to zero when it's the first section in the file (and not 1, which would be
- // the first section -is- the first line of the file).
- LastRightIdx: 0,
- LastLeftIdx: 0,
- RightIdx: 531,
- LeftIdx: 531,
- },
- },
- // We're at the top of the diff but there's content above, so can only expand up.
- expectedResult: DiffLineExpandUp,
- },
- {
- name: "middle of the file with single expansion",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: &DiffLineSectionInfo{
- // Previous section ended at ~500...
- LastRightIdx: 500,
- LastLeftIdx: 500,
- // Next section starts one line away...
- RightIdx: 502,
- LeftIdx: 502,
- // The next block has content (> 0)
- RightHunkSize: 50,
- LeftHunkSize: 50,
- },
- },
- // Can be expanded in a single direction, displaying the missing line (501).
- expectedResult: DiffLineExpandSingle,
- },
- {
- name: "middle of the file with multi line expansion",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: &DiffLineSectionInfo{
- // Previous section ended at ~500...
- LastRightIdx: 500,
- LastLeftIdx: 500,
- // Lines 501-520 are hidden, exactly 20 lines, matching BlobExcerptChunkSize (20)...
- RightIdx: 521,
- LeftIdx: 521,
- // The next block has content (> 0)
- RightHunkSize: 50,
- LeftHunkSize: 50,
- },
- },
- // Can be expanded in a single direction, displaying all the hidden 20 lines.
- expectedResult: DiffLineExpandSingle,
- },
- {
- name: "middle of the file with multi direction expansion",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: &DiffLineSectionInfo{
- // Previous section ended at ~500...
- LastRightIdx: 500,
- LastLeftIdx: 500,
- // Lines 501-521 are hidden, exactly 21 lines, exceeding BlobExcerptChunkSize (20)...
- RightIdx: 522,
- LeftIdx: 522,
- // The next block has content (> 0)
- RightHunkSize: 50,
- LeftHunkSize: 50,
- },
- },
- // Now can be expanded down to display from 501-520 (521 remains hidden), or up to display 502-521 (501
- // remains hidden).
- expectedResult: DiffLineExpandUpDown,
- },
- {
- name: "end of the diff but still file content to display",
- diffLine: &DiffLine{
- Type: DiffLineSection,
- SectionInfo: &DiffLineSectionInfo{
- // We had a previous diff section, of any size/location...
- LastRightIdx: 200,
- LastLeftIdx: 200,
- RightIdx: 531,
- LeftIdx: 531,
- // Hunk size size 0 is a placeholder value for the end or beginning of a file...
- RightHunkSize: 0,
- LeftHunkSize: 0,
- },
- },
- // Combination of conditions says we're at the end of the last diff section, can only expand down.
- expectedResult: DiffLineExpandDown,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := tt.diffLine.GetExpandDirection()
- assert.Equal(t, tt.expectedResult, result)
- })
- }
-}
-
func TestNoCrashes(t *testing.T) {
type testcase struct {
gitdiff string
diff --git a/services/gitdiff/highlightdiff.go b/services/gitdiff/highlightdiff.go
index 61d52d91e6..08681b8617 100644
--- a/services/gitdiff/highlightdiff.go
+++ b/services/gitdiff/highlightdiff.go
@@ -14,14 +14,13 @@ import (
// token is a html tag or entity, eg: "", " ", "<"
func extractHTMLToken(s string) (before, token, after string, valid bool) {
for pos1 := 0; pos1 < len(s); pos1++ {
- switch s[pos1] {
- case '<':
+ if s[pos1] == '<' {
pos2 := strings.IndexByte(s[pos1:], '>')
if pos2 == -1 {
return "", "", s, false
}
return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true
- case '&':
+ } else if s[pos1] == '&' {
pos2 := strings.IndexByte(s[pos1:], ';')
if pos2 == -1 {
return "", "", s, false
diff --git a/services/gitdiff/highlightdiff_test.go b/services/gitdiff/highlightdiff_test.go
index 0070173b9f..2ff4472bcc 100644
--- a/services/gitdiff/highlightdiff_test.go
+++ b/services/gitdiff/highlightdiff_test.go
@@ -43,7 +43,7 @@ func TestDiffWithHighlight(t *testing.T) {
diff.Text = "C"
hcd.recoverOneDiff(&diff)
- assert.Empty(t, diff.Text)
+ assert.Equal(t, "", diff.Text)
}
func TestDiffWithHighlightPlaceholder(t *testing.T) {
@@ -53,8 +53,8 @@ func TestDiffWithHighlightPlaceholder(t *testing.T) {
"a='\U00100000'",
"a='\U0010FFFD''",
)
- assert.Empty(t, hcd.PlaceholderTokenMap[0x00100000])
- assert.Empty(t, hcd.PlaceholderTokenMap[0x0010FFFD])
+ assert.Equal(t, "", hcd.PlaceholderTokenMap[0x00100000])
+ assert.Equal(t, "", hcd.PlaceholderTokenMap[0x0010FFFD])
expected := fmt.Sprintf(`a = ' %s '`, "\U00100000")
output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel)
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 2cac900d41..dedef6cc87 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -5,7 +5,6 @@ package issue
import (
"context"
- "errors"
"fmt"
"forgejo.org/models/db"
@@ -19,7 +18,7 @@ import (
// CreateRefComment creates a commit reference comment to issue.
func CreateRefComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, content, commitSHA string) error {
if len(commitSHA) == 0 {
- return errors.New("cannot create reference with empty commit SHA")
+ return fmt.Errorf("cannot create reference with empty commit SHA")
}
// Check if same reference from same commit has already existed.
@@ -120,28 +119,7 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, contentVersion
// DeleteComment deletes the comment
func DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
- reviewID := comment.ReviewID
-
- err := issues_model.DeleteComment(ctx, comment)
- if err != nil {
- return err
- }
-
- if comment.Review != nil {
- reviewType := comment.Review.Type
- if reviewType == issues_model.ReviewTypePending {
- found, err := db.GetEngine(ctx).Table("comment").Where("review_id = ?", reviewID).Exist()
- if err != nil {
- return err
- } else if !found {
- _, err := db.GetEngine(ctx).Table("review").Where("id = ?", reviewID).Delete()
- if err != nil {
- return err
- }
- }
- }
- }
- return nil
+ return issues_model.DeleteComment(ctx, comment)
})
if err != nil {
return err
diff --git a/services/issue/comments_test.go b/services/issue/comments_test.go
index fcf06d9ec8..728af15529 100644
--- a/services/issue/comments_test.go
+++ b/services/issue/comments_test.go
@@ -8,11 +8,9 @@ import (
"forgejo.org/models/db"
issues_model "forgejo.org/models/issues"
- "forgejo.org/models/moderation"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
webhook_model "forgejo.org/models/webhook"
- "forgejo.org/modules/json"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
issue_service "forgejo.org/services/issue"
@@ -50,9 +48,9 @@ func TestDeleteComment(t *testing.T) {
// Reactions don't exist anymore for this comment.
unittest.AssertNotExistsBean(t, &issues_model.Reaction{CommentID: comment.ID})
// Number of comments was decreased.
- assert.Equal(t, issue.NumComments-1, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).NumComments)
+ assert.EqualValues(t, issue.NumComments-1, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).NumComments)
// A notification was fired for the deletion of this comment.
- assert.Equal(t, hookTaskCount+1, unittest.GetCount(t, &webhook_model.HookTask{}))
+ assert.EqualValues(t, hookTaskCount+1, unittest.GetCount(t, &webhook_model.HookTask{}))
})
t.Run("Comment of pending review", func(t *testing.T) {
@@ -61,7 +59,7 @@ func TestDeleteComment(t *testing.T) {
// We have to ensure that this comment's linked review is pending.
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4}, "review_id != 0")
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: comment.ReviewID})
- assert.Equal(t, issues_model.ReviewTypePending, review.Type)
+ assert.EqualValues(t, issues_model.ReviewTypePending, review.Type)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
require.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, &webhook_model.Webhook{
@@ -71,17 +69,14 @@ func TestDeleteComment(t *testing.T) {
}))
hookTaskCount := unittest.GetCount(t, &webhook_model.HookTask{})
- require.NoError(t, comment.LoadReview(t.Context()))
require.NoError(t, issue_service.DeleteComment(db.DefaultContext, nil, comment))
// The comment doesn't exist anymore.
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
// Ensure that the number of comments wasn't decreased.
- assert.Equal(t, issue.NumComments, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).NumComments)
+ assert.EqualValues(t, issue.NumComments, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).NumComments)
// No notification was fired for the deletion of this comment.
- assert.Equal(t, hookTaskCount, unittest.GetCount(t, &webhook_model.HookTask{}))
- // The review doesn't exist anymore.
- unittest.AssertNotExistsBean(t, &issues_model.Review{ID: comment.ReviewID})
+ assert.EqualValues(t, hookTaskCount, unittest.GetCount(t, &webhook_model.HookTask{}))
})
}
@@ -110,11 +105,11 @@ func TestUpdateComment(t *testing.T) {
newComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
// Content was updated.
- assert.Equal(t, comment.Content, newComment.Content)
+ assert.EqualValues(t, comment.Content, newComment.Content)
// Content version was updated.
- assert.Equal(t, 2, newComment.ContentVersion)
+ assert.EqualValues(t, 2, newComment.ContentVersion)
// A notification was fired for the update of this comment.
- assert.Equal(t, hookTaskCount+1, unittest.GetCount(t, &webhook_model.HookTask{}))
+ assert.EqualValues(t, hookTaskCount+1, unittest.GetCount(t, &webhook_model.HookTask{}))
// Issue history was saved for this comment.
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{CommentID: comment.ID, IsFirstCreated: true, ContentText: oldContent})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{CommentID: comment.ID, ContentText: comment.Content}, "is_first_created = false")
@@ -125,7 +120,7 @@ func TestUpdateComment(t *testing.T) {
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4}, "review_id != 0")
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: comment.ReviewID})
- assert.Equal(t, issues_model.ReviewTypePending, review.Type)
+ assert.EqualValues(t, issues_model.ReviewTypePending, review.Type)
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
unittest.AssertNotExistsBean(t, &issues_model.ContentHistory{CommentID: comment.ID})
require.NoError(t, webhook_model.CreateWebhook(db.DefaultContext, &webhook_model.Webhook{
@@ -141,49 +136,12 @@ func TestUpdateComment(t *testing.T) {
newComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
// Content was updated.
- assert.Equal(t, comment.Content, newComment.Content)
+ assert.EqualValues(t, comment.Content, newComment.Content)
// Content version was updated.
- assert.Equal(t, 2, newComment.ContentVersion)
+ assert.EqualValues(t, 2, newComment.ContentVersion)
// No notification was fired for the update of this comment.
- assert.Equal(t, hookTaskCount, unittest.GetCount(t, &webhook_model.HookTask{}))
+ assert.EqualValues(t, hookTaskCount, unittest.GetCount(t, &webhook_model.HookTask{}))
// Issue history was not saved for this comment.
unittest.AssertNotExistsBean(t, &issues_model.ContentHistory{CommentID: comment.ID})
})
}
-
-func TestCreateShadowCopyOnCommentUpdate(t *testing.T) {
- defer unittest.OverrideFixtures("models/fixtures/ModerationFeatures")()
- require.NoError(t, unittest.PrepareTestDatabase())
-
- userAlexSmithID := int64(1002)
- spamCommentID := int64(18) // posted by @alexsmith
- abuseReportID := int64(1) // submitted for above comment
- newCommentContent := "If anyone needs help, just contact me."
-
- // Retrieve the abusive user (@alexsmith), their SPAM comment and the abuse report already created for this comment.
- poster := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userAlexSmithID})
- comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: spamCommentID, PosterID: poster.ID})
- report := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{
- ID: abuseReportID,
- ContentType: moderation.ReportedContentTypeComment,
- ContentID: comment.ID,
- })
- // The report should not already have a shadow copy linked.
- assert.False(t, report.ShadowCopyID.Valid)
-
- // The abusive user is updating their comment.
- oldContent := comment.Content
- comment.Content = newCommentContent
- require.NoError(t, issue_service.UpdateComment(t.Context(), comment, 0, poster, oldContent))
-
- // Reload the report.
- report = unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{ID: report.ID})
- // A shadow copy should have been created and linked to our report.
- assert.True(t, report.ShadowCopyID.Valid)
- // Retrieve the newly created shadow copy and unmarshal the stored JSON so that we can check the values.
- shadowCopy := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReportShadowCopy{ID: report.ShadowCopyID.Int64})
- shadowCopyCommentData := new(issues_model.CommentData)
- require.NoError(t, json.Unmarshal([]byte(shadowCopy.RawValue), &shadowCopyCommentData))
- // Check to see if the initial content of the comment was stored within the shadow copy.
- assert.Equal(t, oldContent, shadowCopyCommentData.Content)
-}
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 7071a912b0..f6a3e90b10 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -1,12 +1,10 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
-// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issue
import (
"context"
- "errors"
"fmt"
"time"
@@ -61,6 +59,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
// ChangeTitle changes the title of this issue, as the given user.
func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, title string) error {
oldTitle := issue.Title
+ issue.Title = title
if oldTitle == title {
return nil
@@ -74,12 +73,6 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return user_model.ErrBlockedByUser
}
- // If the issue was reported as abusive, a shadow copy should be created before first update.
- if err := issues_model.IfNeededCreateShadowCopyForIssue(ctx, issue); err != nil {
- return err
- }
-
- issue.Title = title
if err := issues_model.ChangeIssueTitle(ctx, issue, doer, oldTitle); err != nil {
return err
}
@@ -259,12 +252,6 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
defer committer.Close()
e := db.GetEngine(ctx)
-
- // If the issue was reported as abusive, a shadow copy should be created before deletion.
- if err := issues_model.IfNeededCreateShadowCopyForIssue(ctx, issue); err != nil {
- return err
- }
-
if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil {
return err
}
@@ -346,13 +333,13 @@ func SetIssueUpdateDate(ctx context.Context, issue *issues_model.Issue, updated
return err
}
if !perm.IsAdmin() && !perm.IsOwner() {
- return errors.New("user needs to have admin or owner right")
+ return fmt.Errorf("user needs to have admin or owner right")
}
// A simple guard against potential inconsistent calls
updatedUnix := timeutil.TimeStamp(updated.Unix())
if updatedUnix < issue.CreatedUnix || updatedUnix > timeutil.TimeStampNow() {
- return errors.New("unallowed update date")
+ return fmt.Errorf("unallowed update date")
}
issue.UpdatedUnix = updatedUnix
diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go
index fb2b2870bd..e15a0118ad 100644
--- a/services/issue/issue_test.go
+++ b/services/issue/issue_test.go
@@ -25,8 +25,8 @@ func TestGetRefEndNamesAndURLs(t *testing.T) {
repoLink := "/foo/bar"
endNames, urls := GetRefEndNamesAndURLs(issues, repoLink)
- assert.Equal(t, map[int64]string{1: "branch1", 2: "tag1", 3: "c0ffee"}, endNames)
- assert.Equal(t, map[int64]string{
+ assert.EqualValues(t, map[int64]string{1: "branch1", 2: "tag1", 3: "c0ffee"}, endNames)
+ assert.EqualValues(t, map[int64]string{
1: repoLink + "/src/branch/branch1",
2: repoLink + "/src/tag/tag1",
3: repoLink + "/src/commit/c0ffee",
diff --git a/services/issue/label.go b/services/issue/label.go
index f18dd67a19..bcac54272a 100644
--- a/services/issue/label.go
+++ b/services/issue/label.go
@@ -8,6 +8,7 @@ import (
"forgejo.org/models/db"
issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
user_model "forgejo.org/models/user"
notify_service "forgejo.org/services/notify"
)
@@ -55,6 +56,17 @@ func RemoveLabel(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return err
}
+ perm, err := access_model.GetUserRepoPermission(dbCtx, issue.Repo, doer)
+ if err != nil {
+ return err
+ }
+ if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
+ if label.OrgID > 0 {
+ return issues_model.ErrOrgLabelNotExist{}
+ }
+ return issues_model.ErrRepoLabelNotExist{}
+ }
+
if err := issues_model.DeleteIssueLabel(dbCtx, issue, label, doer); err != nil {
return err
}
diff --git a/services/issue/milestone.go b/services/issue/milestone.go
index a561bf8eee..3fa7083812 100644
--- a/services/issue/milestone.go
+++ b/services/issue/milestone.go
@@ -5,7 +5,6 @@ package issue
import (
"context"
- "errors"
"fmt"
"forgejo.org/models/db"
@@ -48,7 +47,7 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is
return fmt.Errorf("HasMilestoneByRepoID: %w", err)
}
if !has {
- return errors.New("HasMilestoneByRepoID: issue doesn't exist")
+ return fmt.Errorf("HasMilestoneByRepoID: issue doesn't exist")
}
}
diff --git a/services/issue/pull.go b/services/issue/pull.go
index 6245344ccb..b0a0c47d88 100644
--- a/services/issue/pull.go
+++ b/services/issue/pull.go
@@ -43,6 +43,8 @@ type ReviewRequestNotifier struct {
}
func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
+ files := []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
+
if pr.IsWorkInProgress(ctx) {
return nil, nil
}
@@ -70,17 +72,18 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
return nil, err
}
- var rules []*issues_model.CodeOwnerRule
- for _, file := range []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS", ".forgejo/CODEOWNERS"} {
+ var data string
+ for _, file := range files {
if blob, err := commit.GetBlobByPath(file); err == nil {
- rc, size, err := blob.NewTruncatedReader(setting.UI.MaxDisplayFileSize)
+ data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil {
- rules, _ = issues_model.GetCodeOwnersFromReader(ctx, rc, size > setting.UI.MaxDisplayFileSize)
break
}
}
}
+ rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
+
// get the mergebase
mergeBase, err := getMergeBase(repo, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitRefName())
if err != nil {
diff --git a/services/issue/template.go b/services/issue/template.go
index 07f59af630..67a01825d2 100644
--- a/services/issue/template.go
+++ b/services/issue/template.go
@@ -16,7 +16,7 @@ import (
"forgejo.org/modules/log"
api "forgejo.org/modules/structs"
- "go.yaml.in/yaml/v3"
+ "gopkg.in/yaml.v3"
)
// templateDirCandidates issue templates directory
@@ -31,8 +31,6 @@ var templateDirCandidates = []string{
".github/issue_template",
".gitlab/ISSUE_TEMPLATE",
".gitlab/issue_template",
- "docs/ISSUE_TEMPLATE",
- "docs/issue_template",
}
var templateConfigCandidates = []string{
@@ -42,8 +40,6 @@ var templateConfigCandidates = []string{
".gitea/issue_template/config",
".github/ISSUE_TEMPLATE/config",
".github/issue_template/config",
- "docs/ISSUE_TEMPLATE/config",
- "docs/issue_template/config",
}
func GetDefaultTemplateConfig() api.IssueConfig {
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 17e6d0eec7..8eef62eabe 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -163,12 +163,11 @@ func BatchHandler(ctx *context.Context) {
}
var isUpload bool
- switch br.Operation {
- case "upload":
+ if br.Operation == "upload" {
isUpload = true
- case "download":
+ } else if br.Operation == "download" {
isUpload = false
- default:
+ } else {
log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation)
writeStatus(ctx, http.StatusBadRequest)
return
@@ -595,15 +594,15 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
claims, claimsOk := token.Claims.(*Claims)
if !token.Valid || !claimsOk {
- return nil, errors.New("invalid token claim")
+ return nil, fmt.Errorf("invalid token claim")
}
if claims.RepoID != target.ID {
- return nil, errors.New("invalid token claim")
+ return nil, fmt.Errorf("invalid token claim")
}
if mode == perm.AccessModeWrite && claims.Op != "upload" {
- return nil, errors.New("invalid token claim")
+ return nil, fmt.Errorf("invalid token claim")
}
u, err := user_model.GetUserByID(ctx, claims.UserID)
@@ -616,12 +615,12 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) {
if authorization == "" {
- return nil, errors.New("no token")
+ return nil, fmt.Errorf("no token")
}
parts := strings.SplitN(authorization, " ", 2)
if len(parts) != 2 {
- return nil, errors.New("no token")
+ return nil, fmt.Errorf("no token")
}
tokenSHA := parts[1]
switch strings.ToLower(parts[0]) {
@@ -630,7 +629,7 @@ func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Rep
case "token":
return handleLFSToken(ctx, tokenSHA, target, mode)
}
- return nil, errors.New("token not found")
+ return nil, fmt.Errorf("token not found")
}
func requireAuth(ctx *context.Context) {
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 410fdf6894..c0a37d9fb2 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -685,14 +685,19 @@ func SendRemovedSecurityKey(ctx context.Context, u *user_model.User, securityKey
}
locale := translation.NewLocale(u.Language)
- hasTwoFactor, err := auth_model.HasTwoFactorByUID(ctx, u.ID)
+ hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(ctx, u.ID)
+ if err != nil {
+ return err
+ }
+ hasTOTP, err := auth_model.HasTwoFactorByUID(ctx, u.ID)
if err != nil {
return err
}
data := map[string]any{
"locale": locale,
- "HasTwoFactor": hasTwoFactor,
+ "HasWebAuthn": hasWebAuthn,
+ "HasTOTP": hasTOTP,
"SecurityKeyName": securityKeyName,
"DisplayName": u.DisplayName(),
"Username": u.Name,
diff --git a/services/mailer/mail_actions.go b/services/mailer/mail_actions.go
deleted file mode 100644
index 1119781613..0000000000
--- a/services/mailer/mail_actions.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-package mailer
-
-import (
- "bytes"
-
- actions_model "forgejo.org/models/actions"
- user_model "forgejo.org/models/user"
- "forgejo.org/modules/base"
- "forgejo.org/modules/setting"
- "forgejo.org/modules/translation"
-)
-
-const (
- tplActionNowDone base.TplName = "actions/now_done"
-)
-
-var MailActionRun = mailActionRun // make it mockable
-func mailActionRun(run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
- if setting.MailService == nil {
- // No mail service configured
- return nil
- }
-
- if !run.NotifyEmail {
- return nil
- }
-
- user := run.TriggerUser
- // this happens e.g. when this is a scheduled run
- if user.IsSystem() {
- user = run.Repo.Owner
- }
- if user.IsSystem() || user.Email == "" {
- return nil
- }
-
- if user.EmailNotificationsPreference == user_model.EmailNotificationsDisabled {
- return nil
- }
-
- return sendMailActionRun(user, run, priorStatus, lastRun)
-}
-
-func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
- var (
- locale = translation.NewLocale(to.Language)
- content bytes.Buffer
- )
-
- var subject string
- if run.Status.IsSuccess() {
- subject = locale.TrString("mail.actions.successful_run_after_failure_subject", run.Title, run.Repo.FullName())
- } else {
- subject = locale.TrString("mail.actions.not_successful_run_subject", run.Title, run.Repo.FullName())
- }
-
- commitSHA := run.CommitSHA
- if len(commitSHA) > 7 {
- commitSHA = commitSHA[:7]
- }
-
- data := map[string]any{
- "locale": locale,
- "Link": run.HTMLURL(),
- "Subject": subject,
- "Language": locale.Language(),
- "RepoFullName": run.Repo.FullName(),
- "Run": run,
- "TriggerUserLink": run.TriggerUser.HTMLURL(),
- "LastRun": lastRun,
- "PriorStatus": priorStatus,
- "CommitSHA": commitSHA,
- "IsSuccess": run.Status.IsSuccess(),
- }
-
- if err := bodyTemplates.ExecuteTemplate(&content, string(tplActionNowDone), data); err != nil {
- return err
- }
-
- msg := NewMessage(to.EmailTo(), subject, content.String())
- msg.Info = subject
- SendAsync(msg)
-
- return nil
-}
diff --git a/services/mailer/mail_actions_now_done_test.go b/services/mailer/mail_actions_now_done_test.go
deleted file mode 100644
index e84441f460..0000000000
--- a/services/mailer/mail_actions_now_done_test.go
+++ /dev/null
@@ -1,325 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package mailer
-
-import (
- "slices"
- "testing"
-
- actions_model "forgejo.org/models/actions"
- "forgejo.org/models/db"
- organization_model "forgejo.org/models/organization"
- repo_model "forgejo.org/models/repo"
- user_model "forgejo.org/models/user"
- "forgejo.org/modules/optional"
- "forgejo.org/modules/setting"
- "forgejo.org/modules/test"
- notify_service "forgejo.org/services/notify"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func getActionsNowDoneTestUser(t *testing.T, name, email, notifications string) *user_model.User {
- t.Helper()
- user := new(user_model.User)
- user.Name = name
- user.Language = "en_US"
- user.IsAdmin = false
- user.Email = email
- user.LastLoginUnix = 1693648327
- user.CreatedUnix = 1693648027
- opts := user_model.CreateUserOverwriteOptions{
- AllowCreateOrganization: optional.Some(true),
- EmailNotificationsPreference: ¬ifications,
- }
- require.NoError(t, user_model.AdminCreateUser(db.DefaultContext, user, &opts))
- return user
-}
-
-func getActionsNowDoneTestOrg(t *testing.T, name, email string, owner *user_model.User) *user_model.User {
- t.Helper()
- org := new(organization_model.Organization)
- org.Name = name
- org.Language = "en_US"
- org.IsAdmin = false
- // contact email for the organization, for display purposes but otherwise not used as of v12
- org.Email = email
- org.LastLoginUnix = 1693648327
- org.CreatedUnix = 1693648027
- org.Email = email
- require.NoError(t, organization_model.CreateOrganization(db.DefaultContext, org, owner))
- return (*user_model.User)(org)
-}
-
-func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
- AssertTranslatedLocale(t, msgBody, "mail.actions.successful_run_after_failure", "mail.actions.not_successful_run", "mail.actions.run_info_cur_status", "mail.actions.run_info_sha", "mail.actions.run_info_previous_status", "mail.actions.run_info_trigger", "mail.view_it_on")
-}
-
-func TestActionRunNowDoneStatusMatrix(t *testing.T) {
- successStatuses := []actions_model.Status{
- actions_model.StatusSuccess,
- actions_model.StatusSkipped,
- actions_model.StatusCancelled,
- }
- failureStatuses := []actions_model.Status{
- actions_model.StatusFailure,
- }
-
- for _, testCase := range []struct {
- name string
- statuses []actions_model.Status
- hasLastRun bool
- lastStatuses []actions_model.Status
- run bool
- }{
- {
- name: "FailureNoLastRun",
- statuses: failureStatuses,
- run: true,
- },
- {
- name: "SuccessNoLastRun",
- statuses: successStatuses,
- run: false,
- },
- {
- name: "FailureLastRunSuccess",
- statuses: failureStatuses,
- hasLastRun: true,
- lastStatuses: successStatuses,
- run: true,
- },
- {
- name: "FailureLastRunFailure",
- statuses: failureStatuses,
- hasLastRun: true,
- lastStatuses: failureStatuses,
- run: true,
- },
- {
- name: "SuccessLastRunFailure",
- statuses: successStatuses,
- hasLastRun: true,
- lastStatuses: failureStatuses,
- run: true,
- },
- {
- name: "SuccessLastRunSuccess",
- statuses: successStatuses,
- hasLastRun: true,
- lastStatuses: successStatuses,
- run: false,
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- var called bool
- defer test.MockVariableValue(&MailActionRun, func(run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
- called = true
- return nil
- })()
- for _, status := range testCase.statuses {
- for _, lastStatus := range testCase.lastStatuses {
- called = false
- n := NewNotifier()
- var lastRun *actions_model.ActionRun
- if testCase.hasLastRun {
- lastRun = &actions_model.ActionRun{
- Status: lastStatus,
- }
- }
- n.ActionRunNowDone(t.Context(),
- &actions_model.ActionRun{
- Status: status,
- },
- actions_model.StatusUnknown,
- lastRun)
- assert.Equal(t, testCase.run, called, "status = %s, lastStatus = %s", status, lastStatus)
- }
- }
- })
- }
-}
-
-func TestActionRunNowDoneNotificationMail(t *testing.T) {
- ctx := t.Context()
-
- defer test.MockVariableValue(&setting.Admin.DisableRegularOrgCreation, false)()
-
- actionsUser := user_model.NewActionsUser()
- require.NotEmpty(t, actionsUser.Email)
-
- repo := repo_model.Repository{
- Name: "some repo",
- Description: "rockets are cool",
- }
-
- // Do some funky stuff with the action run's ids:
- // The run with the larger ID finished first.
- // This is odd but something that must work.
- run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"}
- run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"}
-
- assignUsers := func(triggerUser, owner *user_model.User) {
- for _, run := range []*actions_model.ActionRun{run1, run2} {
- run.TriggerUser = triggerUser
- run.TriggerUserID = triggerUser.ID
- run.NotifyEmail = true
- }
- repo.Owner = owner
- repo.OwnerID = owner.ID
- }
-
- notify_service.RegisterNotifier(NewNotifier())
-
- orgOwner := getActionsNowDoneTestUser(t, "org_owner", "org_owner@example.com", "disabled")
- defer CleanUpUsers(ctx, []*user_model.User{orgOwner})
-
- t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) {
- user := getActionsNowDoneTestUser(t, "new_user", "new_user@example.com", "enabled")
- defer CleanUpUsers(ctx, []*user_model.User{user})
- assignUsers(user, user)
- defer MockMailSettings(func(msgs ...*Message) {
- assert.Fail(t, "no mail should be sent")
- })()
- notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
- })
-
- t.Run("WorkflowEnableEmailNotificationIsFalse", func(t *testing.T) {
- user := getActionsNowDoneTestUser(t, "new_user1", "new_user1@example.com", "enabled")
- defer CleanUpUsers(ctx, []*user_model.User{user})
- assignUsers(user, user)
- defer MockMailSettings(func(msgs ...*Message) {
- assert.Fail(t, "no mail should be sent")
- })()
- run2.NotifyEmail = false
- notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
- })
-
- for _, testCase := range []struct {
- name string
- triggerUser *user_model.User
- owner *user_model.User
- expected string
- expectMail bool
- }{
- {
- // if the action is assigned a trigger user in a repository
- // owned by a regular user, the mail is sent to the trigger user
- name: "RegularTriggerUser",
- triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user0", "new_trigger_user0@example.com", user_model.EmailNotificationsEnabled),
- owner: getActionsNowDoneTestUser(t, "new_owner0", "new_owner0@example.com", user_model.EmailNotificationsEnabled),
- expected: "trigger",
- expectMail: true,
- },
- {
- // if the action is assigned to a system user (e.g. ActionsUser)
- // in a repository owned by a regular user, the mail is sent to
- // the user that owns the repository
- name: "SystemTriggerUserAndRegularOwner",
- triggerUser: actionsUser,
- owner: getActionsNowDoneTestUser(t, "new_owner1", "new_owner1@example.com", user_model.EmailNotificationsEnabled),
- expected: "owner",
- expectMail: true,
- },
- {
- // if the action is assigned a trigger user with disabled notifications in a repository
- // owned by a regular user, no mail is sent
- name: "RegularTriggerUserNotificationsDisabled",
- triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user2", "new_trigger_user2@example.com", user_model.EmailNotificationsDisabled),
- owner: getActionsNowDoneTestUser(t, "new_owner2", "new_owner2@example.com", user_model.EmailNotificationsEnabled),
- expectMail: false,
- },
- {
- // if the action is assigned to a system user (e.g. ActionsUser)
- // owned by a regular user with disabled notifications, no mail is sent
- name: "SystemTriggerUserAndRegularOwnerNotificationsDisabled",
- triggerUser: actionsUser,
- owner: getActionsNowDoneTestUser(t, "new_owner3", "new_owner3@example.com", user_model.EmailNotificationsDisabled),
- expectMail: false,
- },
- {
- // if the action is assigned to a system user (e.g. ActionsUser)
- // in a repository owned by an organization with an email contact, the mail is sent to
- // this email contact
- name: "SystemTriggerUserAndOrgOwner",
- triggerUser: actionsUser,
- owner: getActionsNowDoneTestOrg(t, "new_org1", "new_org_owner0@example.com", orgOwner),
- expected: "owner",
- expectMail: true,
- },
- {
- // if the action is assigned to a system user (e.g. ActionsUser)
- // in a repository owned by an organization without an email contact, no mail is sent
- name: "SystemTriggerUserAndNoMailOrgOwner",
- triggerUser: actionsUser,
- owner: getActionsNowDoneTestOrg(t, "new_org2", "", orgOwner),
- expectMail: false,
- },
- } {
- t.Run(testCase.name, func(t *testing.T) {
- assignUsers(testCase.triggerUser, testCase.owner)
- defer CleanUpUsers(ctx, slices.DeleteFunc([]*user_model.User{testCase.triggerUser, testCase.owner}, func(user *user_model.User) bool {
- return user.IsSystem()
- }))
-
- t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) {
- mailSent := false
- defer MockMailSettings(func(msgs ...*Message) {
- assert.Len(t, msgs, 1)
- msg := msgs[0]
- assert.False(t, mailSent, "sent mail twice")
- expectedEmail := testCase.triggerUser.Email
- if testCase.expected == "owner" { // otherwise "trigger"
- expectedEmail = testCase.owner.Email
- }
- require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
- mailSent = true
- assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
- assert.Contains(t, msg.Body, testCase.triggerUser.Name)
- // what happened
- assert.Contains(t, msg.Body, "failed")
- // new status of run
- assert.Contains(t, msg.Body, "failure")
- // prior status of this run
- assert.Contains(t, msg.Body, "waiting")
- assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
- })()
- require.NotNil(t, setting.MailService)
-
- notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil)
- assert.Equal(t, testCase.expectMail, mailSent)
- })
-
- t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) {
- mailSent := false
- defer MockMailSettings(func(msgs ...*Message) {
- assert.Len(t, msgs, 1)
- msg := msgs[0]
- assert.False(t, mailSent, "sent mail twice")
- expectedEmail := testCase.triggerUser.Email
- if testCase.expected == "owner" { // otherwise "trigger"
- expectedEmail = testCase.owner.Email
- }
- require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
- mailSent = true
- assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
- assert.Contains(t, msg.Body, testCase.triggerUser.Name)
- // what happened
- assert.Contains(t, msg.Body, "recovered")
- // old status of run
- assert.Contains(t, msg.Body, "failure")
- // new status of run
- assert.Contains(t, msg.Body, "success")
- // prior status of this run
- assert.Contains(t, msg.Body, "running")
- })()
- require.NotNil(t, setting.MailService)
-
- notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1)
- assert.Equal(t, testCase.expectMail, mailSent)
- })
- })
- }
-}
diff --git a/services/mailer/mail_admin_new_user_test.go b/services/mailer/mail_admin_new_user_test.go
index 58afcfcda6..9273691792 100644
--- a/services/mailer/mail_admin_new_user_test.go
+++ b/services/mailer/mail_admin_new_user_test.go
@@ -4,6 +4,7 @@
package mailer
import (
+ "context"
"strconv"
"testing"
@@ -16,7 +17,7 @@ import (
"github.com/stretchr/testify/require"
)
-func getAdminNewUserTestUsers(t *testing.T) []*user_model.User {
+func getTestUsers(t *testing.T) []*user_model.User {
t.Helper()
admin := new(user_model.User)
admin.Name = "testadmin"
@@ -37,10 +38,16 @@ func getAdminNewUserTestUsers(t *testing.T) []*user_model.User {
return []*user_model.User{admin, newUser}
}
+func cleanUpUsers(ctx context.Context, users []*user_model.User) {
+ for _, u := range users {
+ db.DeleteByID[user_model.User](ctx, u.ID)
+ }
+}
+
func TestAdminNotificationMail_test(t *testing.T) {
ctx := t.Context()
- users := getAdminNewUserTestUsers(t)
+ users := getTestUsers(t)
t.Run("SendNotificationEmailOnNewUser_true", func(t *testing.T) {
defer test.MockVariableValue(&setting.Admin.SendNotificationEmailOnNewUser, true)()
@@ -68,5 +75,5 @@ func TestAdminNotificationMail_test(t *testing.T) {
MailNewUser(ctx, users[1])
})
- CleanUpUsers(ctx, users)
+ cleanUpUsers(ctx, users)
}
diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go
index 0d8e054041..b0329caa0b 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -85,7 +85,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo
// =========== Repo watchers ===========
// Make repo watchers last, since it's likely the list with the most users
- if !ctx.Issue.IsPull || !ctx.Issue.PullRequest.IsWorkInProgress(ctx) || ctx.ActionType == activities_model.ActionCreatePullRequest {
+ if !(ctx.Issue.IsPull && ctx.Issue.PullRequest.IsWorkInProgress(ctx) && ctx.ActionType != activities_model.ActionCreatePullRequest) {
ids, err = repo_model.GetRepoWatchersIDs(ctx, ctx.Issue.RepoID)
if err != nil {
return fmt.Errorf("GetRepoWatchersIDs(%d): %w", ctx.Issue.RepoID, err)
@@ -137,8 +137,9 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi
}
// At this point we exclude:
// user that don't have all mails enabled or users only get mail on mention and this is one ...
- if user.EmailNotificationsPreference != user_model.EmailNotificationsEnabled &&
- user.EmailNotificationsPreference != user_model.EmailNotificationsAndYourOwn && (!fromMention || user.EmailNotificationsPreference != user_model.EmailNotificationsOnMention) {
+ if !(user.EmailNotificationsPreference == user_model.EmailNotificationsEnabled ||
+ user.EmailNotificationsPreference == user_model.EmailNotificationsAndYourOwn ||
+ fromMention && user.EmailNotificationsPreference == user_model.EmailNotificationsOnMention) {
continue
}
diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go
index 5375133415..a2a871d3c3 100644
--- a/services/mailer/mail_team_invite.go
+++ b/services/mailer/mail_team_invite.go
@@ -6,7 +6,6 @@ package mailer
import (
"bytes"
"context"
- "errors"
"fmt"
"net/url"
@@ -40,7 +39,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
if err != nil && !user_model.IsErrUserNotExist(err) {
return err
} else if user != nil && user.ProhibitLogin {
- return errors.New("login is prohibited for the invited user")
+ return fmt.Errorf("login is prohibited for the invited user")
}
inviteRedirect := url.QueryEscape(fmt.Sprintf("/org/invite/%s", invite.Token))
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index afbcb8064e..616eea2d85 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -494,7 +494,8 @@ func Test_createReference(t *testing.T) {
func TestFromDisplayName(t *testing.T) {
template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
require.NoError(t, err)
- defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromDisplayNameFormatTemplate: template})()
+ setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
+ defer func() { setting.MailService = nil }()
tests := []struct {
userDisplayName string
@@ -517,18 +518,24 @@ func TestFromDisplayName(t *testing.T) {
t.Run(tc.userDisplayName, func(t *testing.T) {
user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"}
got := fromDisplayName(user)
- assert.Equal(t, tc.fromDisplayName, got)
+ assert.EqualValues(t, tc.fromDisplayName, got)
})
}
t.Run("template with all available vars", func(t *testing.T) {
template, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
require.NoError(t, err)
- defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromDisplayNameFormatTemplate: template})()
- defer test.MockVariableValue(&setting.AppName, "Code IT")()
- defer test.MockVariableValue(&setting.Domain, "code.it")()
+ setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
+ oldAppName := setting.AppName
+ setting.AppName = "Code IT"
+ oldDomain := setting.Domain
+ setting.Domain = "code.it"
+ defer func() {
+ setting.AppName = oldAppName
+ setting.Domain = oldDomain
+ }()
- assert.Equal(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
+ assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
})
}
diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go
index d8646d9ddd..4561240df5 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -8,7 +8,6 @@ import (
"bytes"
"context"
"crypto/tls"
- "errors"
"fmt"
"hash/fnv"
"io"
@@ -29,7 +28,7 @@ import (
notify_service "forgejo.org/services/notify"
ntlmssp "github.com/Azure/go-ntlmssp"
- "github.com/inbucket/html2text"
+ "github.com/jaytaylor/html2text"
"gopkg.in/gomail.v2"
)
@@ -177,7 +176,7 @@ func (a *ntlmAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
func (a *ntlmAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
if len(fromServer) == 0 {
- return nil, errors.New("ntlm ChallengeMessage is empty")
+ return nil, fmt.Errorf("ntlm ChallengeMessage is empty")
}
authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded)
return authenticateMessage, err
@@ -265,7 +264,7 @@ func (s *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
canAuth, options := client.Extension("AUTH")
if len(opts.User) > 0 {
if !canAuth {
- return errors.New("SMTP server does not support AUTH, but credentials provided")
+ return fmt.Errorf("SMTP server does not support AUTH, but credentials provided")
}
var auth smtp.Auth
diff --git a/services/mailer/mailer_test.go b/services/mailer/mailer_test.go
index 34fd847c05..aef242d908 100644
--- a/services/mailer/mailer_test.go
+++ b/services/mailer/mailer_test.go
@@ -72,7 +72,7 @@ func TestToMessage(t *testing.T) {
_, err := m1.ToMessage().WriteTo(buf)
require.NoError(t, err)
header, _ := extractMailHeaderAndContent(t, buf.String())
- assert.Equal(t, map[string]string{
+ assert.EqualValues(t, map[string]string{
"Content-Type": "multipart/alternative;",
"Date": "Mon, 01 Jan 0001 00:00:00 +0000",
"From": "\"Test Gitea\" ",
@@ -92,7 +92,7 @@ func TestToMessage(t *testing.T) {
_, err = m1.ToMessage().WriteTo(buf)
require.NoError(t, err)
header, _ = extractMailHeaderAndContent(t, buf.String())
- assert.Equal(t, map[string]string{
+ assert.EqualValues(t, map[string]string{
"Content-Type": "multipart/alternative;",
"Date": "Mon, 01 Jan 0001 00:00:00 +0000",
"From": "\"Test Gitea\" ",
diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go
index 5e9cbe3e99..9ef71dbdb3 100644
--- a/services/mailer/main_test.go
+++ b/services/mailer/main_test.go
@@ -7,10 +7,7 @@ import (
"context"
"testing"
- "forgejo.org/models/db"
- organization_model "forgejo.org/models/organization"
"forgejo.org/models/unittest"
- user_model "forgejo.org/models/user"
"forgejo.org/modules/setting"
"forgejo.org/modules/templates"
"forgejo.org/modules/test"
@@ -49,14 +46,3 @@ func MockMailSettings(send func(msgs ...*Message)) func() {
}
}
}
-
-func CleanUpUsers(ctx context.Context, users []*user_model.User) {
- for _, u := range users {
- if u.IsOrganization() {
- organization_model.DeleteOrganization(ctx, (*organization_model.Organization)(u))
- } else {
- db.DeleteByID[user_model.User](ctx, u.ID)
- db.DeleteByBean(ctx, &user_model.EmailAddress{UID: u.ID})
- }
- }
-}
diff --git a/services/mailer/notify.go b/services/mailer/notify.go
index 640de31fcc..e61ecd0511 100644
--- a/services/mailer/notify.go
+++ b/services/mailer/notify.go
@@ -7,7 +7,6 @@ import (
"context"
"fmt"
- actions_model "forgejo.org/models/actions"
activities_model "forgejo.org/models/activities"
issues_model "forgejo.org/models/issues"
repo_model "forgejo.org/models/repo"
@@ -31,16 +30,15 @@ func (m *mailNotifier) CreateIssueComment(ctx context.Context, doer *user_model.
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
) {
var act activities_model.ActionType
- switch comment.Type {
- case issues_model.CommentTypeClose:
+ if comment.Type == issues_model.CommentTypeClose {
act = activities_model.ActionCloseIssue
- case issues_model.CommentTypeReopen:
+ } else if comment.Type == issues_model.CommentTypeReopen {
act = activities_model.ActionReopenIssue
- case issues_model.CommentTypeComment:
+ } else if comment.Type == issues_model.CommentTypeComment {
act = activities_model.ActionCommentIssue
- case issues_model.CommentTypeCode:
+ } else if comment.Type == issues_model.CommentTypeCode {
act = activities_model.ActionCommentIssue
- case issues_model.CommentTypePullRequestPush:
+ } else if comment.Type == issues_model.CommentTypePullRequestPush {
act = 0
}
@@ -96,12 +94,11 @@ func (m *mailNotifier) NewPullRequest(ctx context.Context, pr *issues_model.Pull
func (m *mailNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, r *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) {
var act activities_model.ActionType
- switch comment.Type {
- case issues_model.CommentTypeClose:
+ if comment.Type == issues_model.CommentTypeClose {
act = activities_model.ActionCloseIssue
- case issues_model.CommentTypeReopen:
+ } else if comment.Type == issues_model.CommentTypeReopen {
act = activities_model.ActionReopenIssue
- case issues_model.CommentTypeComment:
+ } else if comment.Type == issues_model.CommentTypeComment {
act = activities_model.ActionCommentPull
}
if err := MailParticipantsComment(ctx, comment, act, pr.Issue, mentions); err != nil {
@@ -209,13 +206,3 @@ func (m *mailNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *
func (m *mailNotifier) NewUserSignUp(ctx context.Context, newUser *user_model.User) {
MailNewUser(ctx, newUser)
}
-
-func (m *mailNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) {
- // Only send a mail on a successful run when the workflow recovered (i.e., the run before failed).
- if !run.Status.IsFailure() && (lastRun == nil || !lastRun.Status.IsFailure()) {
- return
- }
- if err := MailActionRun(run, priorStatus, lastRun); err != nil {
- log.Error("MailActionRunNowDone: %v", err)
- }
-}
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 2f1b1e738c..b5fcd78cb7 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -5,7 +5,7 @@ package markup
import (
"context"
- "errors"
+ "fmt"
"forgejo.org/models/perm/access"
"forgejo.org/models/repo"
@@ -55,7 +55,7 @@ func ProcessorHelper() *markup.ProcessorHelper {
return nil, err
}
if !perms.CanRead(unit.TypeCode) {
- return nil, errors.New("cannot access repository code")
+ return nil, fmt.Errorf("cannot access repository code")
}
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
diff --git a/services/migrations/dump.go b/services/migrations/dump.go
index af161bc73d..cbf6b87668 100644
--- a/services/migrations/dump.go
+++ b/services/migrations/dump.go
@@ -25,7 +25,7 @@ import (
"forgejo.org/modules/structs"
"github.com/google/uuid"
- "go.yaml.in/yaml/v3"
+ "gopkg.in/yaml.v3"
)
var _ base.Uploader = &RepositoryDumper{}
diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go
index a9bd2dafb0..b68fc01083 100644
--- a/services/migrations/gitbucket.go
+++ b/services/migrations/gitbucket.go
@@ -71,7 +71,7 @@ func (g *GitBucketDownloader) LogString() string {
// NewGitBucketDownloader creates a GitBucket downloader
func NewGitBucketDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GitBucketDownloader {
- githubDownloader := NewGithubDownloaderV3(ctx, baseURL, true, true, userName, password, token, repoOwner, repoName)
+ githubDownloader := NewGithubDownloaderV3(ctx, baseURL, userName, password, token, repoOwner, repoName)
// Gitbucket 4.40 uses different internal hard-coded perPage values.
// Issues, PRs, and other major parts use 25. Release page uses 10.
// Some API doesn't support paging yet. Sounds difficult, but using
diff --git a/services/migrations/gitbucket_test.go b/services/migrations/gitbucket_test.go
deleted file mode 100644
index 3cb69d65c9..0000000000
--- a/services/migrations/gitbucket_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package migrations
-
-import (
- "os"
- "testing"
-
- "forgejo.org/models/unittest"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestGitbucketDownloaderCreation(t *testing.T) {
- token := os.Getenv("GITHUB_READ_TOKEN")
- fixturePath := "./testdata/github/full_download"
- server := unittest.NewMockWebServer(t, "https://api.github.com", fixturePath, false)
- defer server.Close()
-
- downloader := NewGitBucketDownloader(t.Context(), server.URL, "", "", token, "forgejo", "test_repo")
- err := downloader.RefreshRate()
- require.NoError(t, err)
-}
diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go
index 5acc3b86a9..24c53af023 100644
--- a/services/migrations/gitea_downloader_test.go
+++ b/services/migrations/gitea_downloader_test.go
@@ -45,7 +45,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
topics, err := downloader.GetTopics()
require.NoError(t, err)
sort.Strings(topics)
- assert.Equal(t, []string{"ci", "gitea", "migration", "test"}, topics)
+ assert.EqualValues(t, []string{"ci", "gitea", "migration", "test"}, topics)
labels, err := downloader.GetLabels()
require.NoError(t, err)
@@ -132,7 +132,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
require.NoError(t, err)
assert.True(t, isEnd)
assert.Len(t, issues, 7)
- assert.Equal(t, "open", issues[0].State)
+ assert.EqualValues(t, "open", issues[0].State)
issues, isEnd, err = downloader.GetIssues(3, 2)
require.NoError(t, err)
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 7887dacdb1..55adad9685 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -766,7 +766,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model
issue := issues_model.Issue{
RepoID: g.repo.ID,
Repo: g.repo,
- Title: util.TruncateRunes(prTitle, 255),
+ Title: prTitle,
Index: pr.Number,
Content: pr.Content,
MilestoneID: milestoneID,
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index e33d597cdc..e07c621acc 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -19,6 +19,7 @@ import (
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
"forgejo.org/modules/log"
base "forgejo.org/modules/migration"
"forgejo.org/modules/optional"
@@ -29,130 +30,6 @@ import (
"github.com/stretchr/testify/require"
)
-func TestCommentUpload(t *testing.T) {
- unittest.PrepareTestEnv(t)
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- var (
- opts = base.MigrateOptions{
- Issues: true,
- }
- repoName = "test_repo"
- uploader = NewGiteaLocalUploader(t.Context(), user, user.Name, repoName)
- )
- defer uploader.Close()
-
- fixturePath := "./testdata/github/full_download"
- server := unittest.NewMockWebServer(t, "https://api.github.com", fixturePath, false)
- defer server.Close()
-
- // Mock Data
- repoMock := &base.Repository{
- Name: repoName,
- Owner: "forgejo",
- Description: "Some mock repo",
- CloneURL: server.URL + "/forgejo/test_repo.git",
- OriginalURL: server.URL + "/forgejo/test_repo",
- DefaultBranch: "master",
- Website: "https://codeberg.org/forgejo/forgejo/",
- }
-
- // Create Repo
- require.NoError(t, uploader.CreateRepo(repoMock, opts))
-
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: repoName})
-
- // Create and Test Issues Uploading
- issueA := &base.Issue{
- Title: "First issue",
- Number: 0,
- PosterID: 37243484,
- PosterName: "PatDyn",
- PosterEmail: "",
- Content: "Mock Content",
- Milestone: "Mock Milestone",
- State: "open",
- Created: time.Date(2025, 8, 7, 12, 44, 7, 0, time.UTC),
- Updated: time.Date(2025, 8, 7, 12, 44, 47, 0, time.UTC),
- Labels: nil,
- Reactions: nil,
- Closed: nil,
- IsLocked: false,
- Assignees: nil,
- ForeignIndex: 0,
- }
-
- issueB := &base.Issue{
- Title: "Second Issue",
- Number: 1,
- PosterID: 37243484,
- PosterName: "PatDyn",
- PosterEmail: "",
- Content: "Mock Content",
- Milestone: "Mock Milestone",
- State: "open",
- Created: time.Date(2025, 8, 7, 12, 45, 44, 0, time.UTC),
- Updated: time.Date(2025, 8, 7, 13, 7, 25, 0, time.UTC),
- Labels: nil,
- Reactions: nil,
- Closed: nil,
- IsLocked: false,
- Assignees: nil,
- ForeignIndex: 1,
- }
-
- err := uploader.CreateIssues(issueA, issueB)
- require.NoError(t, err)
-
- issues, err := issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
- RepoIDs: []int64{repo.ID},
- IsPull: optional.Some(false),
- SortType: "newest",
- })
- require.NoError(t, err)
- assert.Len(t, issues, 2)
-
- // Create and Test Comment Uploading
- issueAComment := &base.Comment{
- IssueIndex: 0,
- Index: 0,
- CommentType: "comment",
- PosterID: 37243484,
- PosterName: "PatDyn",
- PosterEmail: "",
- Created: time.Date(2025, 8, 7, 12, 44, 24, 0, time.UTC),
- Updated: time.Date(2025, 8, 7, 12, 44, 24, 0, time.UTC),
- Content: "First Mock Comment",
- Reactions: nil,
- Meta: nil,
- }
- issueBComment := &base.Comment{
- IssueIndex: 1,
- Index: 1,
- CommentType: "comment",
- PosterID: 37243484,
- PosterName: "PatDyn",
- PosterEmail: "",
- Created: time.Date(2025, 8, 7, 13, 7, 25, 0, time.UTC),
- Updated: time.Date(2025, 8, 7, 13, 7, 25, 0, time.UTC),
- Content: "Second Mock Comment",
- Reactions: nil,
- Meta: nil,
- }
- require.NoError(t, uploader.CreateComments(issueBComment, issueAComment))
-
- issues, err = issues_model.Issues(db.DefaultContext, &issues_model.IssuesOptions{
- RepoIDs: []int64{repo.ID},
- IsPull: optional.Some(false),
- SortType: "newest",
- })
- require.NoError(t, err)
- assert.Len(t, issues, 2)
- require.NoError(t, issues[0].LoadDiscussComments(db.DefaultContext))
- require.NoError(t, issues[1].LoadDiscussComments(db.DefaultContext))
- assert.Len(t, issues[0].Comments, 1)
- assert.Len(t, issues[1].Comments, 1)
-}
-
func TestGiteaUploadRepo(t *testing.T) {
// FIXME: Since no accesskey or user/password will trigger rate limit of github, just skip
t.Skip()
@@ -163,9 +40,9 @@ func TestGiteaUploadRepo(t *testing.T) {
var (
ctx = t.Context()
- downloader = NewGithubDownloaderV3(ctx, "https://github.com", true, true, "", "", "", "go-xorm", "builder")
+ downloader = NewGithubDownloaderV3(ctx, "https://github.com", "", "", "", "go-xorm", "builder")
repoName = "builder-" + time.Now().Format("2006-01-02-15-04-05")
- uploader = NewGiteaLocalUploader(t.Context(), user, user.Name, repoName)
+ uploader = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName)
)
err := migrateRepository(db.DefaultContext, user, downloader, uploader, base.MigrateOptions{
@@ -187,7 +64,7 @@ func TestGiteaUploadRepo(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: repoName})
assert.True(t, repo.HasWiki())
- assert.Equal(t, repo_model.RepositoryReady, repo.Status)
+ assert.EqualValues(t, repo_model.RepositoryReady, repo.Status)
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
@@ -296,7 +173,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
uploader.userMap = make(map[int64]int64)
err = uploader.remapUser(&source, &target)
require.NoError(t, err)
- assert.Equal(t, user.ID, target.GetUserID())
+ assert.EqualValues(t, user.ID, target.GetUserID())
}
func TestGiteaUploadRemapExternalUser(t *testing.T) {
@@ -347,7 +224,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
target = repo_model.Release{}
err = uploader.remapUser(&source, &target)
require.NoError(t, err)
- assert.Equal(t, linkedUser.ID, target.GetUserID())
+ assert.EqualValues(t, linkedUser.ID, target.GetUserID())
}
func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
@@ -627,14 +504,14 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
head, err := uploader.updateGitForPullRequest(&testCase.pr)
require.NoError(t, err)
- assert.Equal(t, testCase.head, head)
+ assert.EqualValues(t, testCase.head, head)
log.Info(stopMark)
logFiltered, logStopped := logChecker.Check(5 * time.Second)
assert.True(t, logStopped)
if len(testCase.logFilter) > 0 {
- assert.Equal(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
+ assert.EqualValues(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
}
})
}
diff --git a/services/migrations/github.go b/services/migrations/github.go
index 1fe5d2cc8e..5052a68114 100644
--- a/services/migrations/github.go
+++ b/services/migrations/github.go
@@ -57,7 +57,7 @@ func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOp
log.Trace("Create github downloader BaseURL: %s %s/%s", baseURL, oldOwner, oldName)
- return NewGithubDownloaderV3(ctx, baseURL, opts.PullRequests, opts.Issues, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
+ return NewGithubDownloaderV3(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
}
// GitServiceType returns the type of git service
@@ -69,34 +69,30 @@ func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType {
// from github via APIv3
type GithubDownloaderV3 struct {
base.NullDownloader
- ctx context.Context
- clients []*github.Client
- baseURL string
- repoOwner string
- repoName string
- userName string
- password string
- getPullRequests bool
- getIssues bool
- rates []*github.Rate
- curClientIdx int
- maxPerPage int
- SkipReactions bool
- SkipReviews bool
+ ctx context.Context
+ clients []*github.Client
+ baseURL string
+ repoOwner string
+ repoName string
+ userName string
+ password string
+ rates []*github.Rate
+ curClientIdx int
+ maxPerPage int
+ SkipReactions bool
+ SkipReviews bool
}
// NewGithubDownloaderV3 creates a github Downloader via github v3 API
-func NewGithubDownloaderV3(ctx context.Context, baseURL string, getPullRequests, getIssues bool, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
+func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
downloader := GithubDownloaderV3{
- userName: userName,
- baseURL: baseURL,
- password: password,
- ctx: ctx,
- repoOwner: repoOwner,
- repoName: repoName,
- maxPerPage: 100,
- getPullRequests: getPullRequests,
- getIssues: getIssues,
+ userName: userName,
+ baseURL: baseURL,
+ password: password,
+ ctx: ctx,
+ repoOwner: repoOwner,
+ repoName: repoName,
+ maxPerPage: 100,
}
if token != "" {
@@ -144,7 +140,7 @@ func (g *GithubDownloaderV3) LogString() string {
func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
githubClient := github.NewClient(client)
if baseURL != "https://github.com" {
- githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL)
+ githubClient, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL)
}
g.clients = append(g.clients, githubClient)
g.rates = append(g.rates, nil)
@@ -368,8 +364,7 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
// Prevent open redirect
if !hasBaseURL(redirectURL, g.baseURL) &&
- !hasBaseURL(redirectURL, "https://objects.githubusercontent.com/") &&
- !hasBaseURL(redirectURL, "https://release-assets.githubusercontent.com/") {
+ !hasBaseURL(redirectURL, "https://objects.githubusercontent.com/") {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.GetID(), g, redirectURL)
return io.NopCloser(strings.NewReader(redirectURL)), nil
@@ -586,24 +581,6 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
return allComments, nil
}
-func (g *GithubDownloaderV3) filterByHTMLURL(comments []*github.IssueComment, filterBy string) []*github.IssueComment {
- var result []*github.IssueComment
- for _, val := range comments {
- if !strings.Contains(*val.HTMLURL, filterBy) {
- result = append(result, val)
- }
- }
- return result
-}
-
-func (g *GithubDownloaderV3) filterPRComments(comments []*github.IssueComment) []*github.IssueComment {
- return g.filterByHTMLURL(comments, "/pull/")
-}
-
-func (g *GithubDownloaderV3) filterIssueComments(comments []*github.IssueComment) []*github.IssueComment {
- return g.filterByHTMLURL(comments, "/issues/")
-}
-
// GetAllComments returns repository comments according page and perPageSize
func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, bool, error) {
var (
@@ -630,12 +607,6 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
}
isEnd := resp.NextPage == 0
- if g.getIssues && !g.getPullRequests {
- comments = g.filterPRComments(comments)
- } else if !g.getIssues && g.getPullRequests {
- comments = g.filterIssueComments(comments)
- }
-
log.Trace("Request get comments %d/%d, but in fact get %d, next page is %d", perPage, page, len(comments), resp.NextPage)
g.setRate(&resp.Rate)
for _, comment := range comments {
@@ -914,18 +885,3 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
}
return allReviews, nil
}
-
-// FormatCloneURL add authentication into remote URLs
-func (g *GithubDownloaderV3) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
- u, err := url.Parse(remoteAddr)
- if err != nil {
- return "", err
- }
- if len(opts.AuthToken) > 0 {
- // "multiple tokens" are used to benefit more "API rate limit quota"
- // git clone doesn't count for rate limits, so only use the first token.
- // source: https://github.com/orgs/community/discussions/44515
- u.User = url.UserPassword("oauth2", strings.Split(opts.AuthToken, ",")[0])
- }
- return u.String(), nil
-}
diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go
index ef2850d2d2..b1f20c4716 100644
--- a/services/migrations/github_test.go
+++ b/services/migrations/github_test.go
@@ -12,100 +12,19 @@ import (
"forgejo.org/models/unittest"
base "forgejo.org/modules/migration"
- "github.com/google/go-github/v64/github"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func TestGithubDownloaderFilterComments(t *testing.T) {
- GithubLimitRateRemaining = 3 // Wait at 3 remaining since we could have 3 CI in //
-
- token := os.Getenv("GITHUB_READ_TOKEN")
- fixturePath := "./testdata/github/full_download"
- server := unittest.NewMockWebServer(t, "https://api.github.com", fixturePath, false)
- defer server.Close()
-
- downloader := NewGithubDownloaderV3(t.Context(), server.URL, true, true, "", "", token, "forgejo", "test_repo")
- err := downloader.RefreshRate()
- require.NoError(t, err)
-
- var githubComments []*github.IssueComment
- issueID := int64(7)
- iNodeID := "MDEyOklzc3VlQ29tbWVudDE=" // "IssueComment1"
- iBody := "Hello"
- iCreated := new(github.Timestamp)
- iUpdated := new(github.Timestamp)
- iCreated.Time = time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
- iUpdated.Time = time.Date(2025, 1, 1, 12, 1, 0, 0, time.UTC)
- iAssociation := "COLLABORATOR"
- iURL := "https://api.github.com/repos/forgejo/test_repo/issues/comments/3164032267"
- iHTMLURL := "https://github.com/forgejo/test_repo/issues/1#issuecomment-3164032267"
- iIssueURL := "https://api.github.com/repos/forgejo/test_repo/issues/1"
-
- githubComments = append(githubComments,
- &github.IssueComment{
- ID: &issueID,
- NodeID: &iNodeID,
- Body: &iBody,
- Reactions: nil,
- CreatedAt: iCreated,
- UpdatedAt: iUpdated,
- AuthorAssociation: &iAssociation,
- URL: &iURL,
- HTMLURL: &iHTMLURL,
- IssueURL: &iIssueURL,
- },
- )
-
- prID := int64(4)
- pNodeID := "IC_kwDOPQx9Mc65LHhx"
- pBody := "Hello"
- pCreated := new(github.Timestamp)
- pUpdated := new(github.Timestamp)
- pCreated.Time = time.Date(2025, 1, 1, 11, 0, 0, 0, time.UTC)
- pUpdated.Time = time.Date(2025, 1, 1, 11, 1, 0, 0, time.UTC)
- pAssociation := "COLLABORATOR"
- pURL := "https://api.github.com/repos/forgejo/test_repo/issues/comments/3164118916"
- pHTMLURL := "https://github.com/forgejo/test_repo/pull/3#issuecomment-3164118916"
- pIssueURL := "https://api.github.com/repos/forgejo/test_repo/issues/3"
-
- githubComments = append(githubComments, &github.IssueComment{
- ID: &prID,
- NodeID: &pNodeID,
- Body: &pBody,
- Reactions: nil,
- CreatedAt: pCreated,
- UpdatedAt: pUpdated,
- AuthorAssociation: &pAssociation,
- URL: &pURL,
- HTMLURL: &pHTMLURL,
- IssueURL: &pIssueURL,
- })
-
- filteredComments := downloader.filterPRComments(githubComments)
-
- // Check each issue index not being from the PR
- for _, comment := range filteredComments {
- assert.NotEqual(t, *comment.ID, prID)
- }
-
- filteredComments = downloader.filterIssueComments(githubComments)
-
- // Check each issue index not being from the issue
- for _, comment := range filteredComments {
- assert.NotEqual(t, *comment.ID, issueID)
- }
-}
-
func TestGitHubDownloadRepo(t *testing.T) {
GithubLimitRateRemaining = 3 // Wait at 3 remaining since we could have 3 CI in //
token := os.Getenv("GITHUB_READ_TOKEN")
fixturePath := "./testdata/github/full_download"
- server := unittest.NewMockWebServer(t, "https://api.github.com", fixturePath, false)
+ server := unittest.NewMockWebServer(t, "https://api.github.com", fixturePath, token != "")
defer server.Close()
- downloader := NewGithubDownloaderV3(t.Context(), server.URL, true, true, "", "", token, "forgejo", "test_repo")
+ downloader := NewGithubDownloaderV3(t.Context(), server.URL, "", "", token, "go-gitea", "test_repo")
err := downloader.RefreshRate()
require.NoError(t, err)
@@ -113,44 +32,38 @@ func TestGitHubDownloadRepo(t *testing.T) {
require.NoError(t, err)
assertRepositoryEqual(t, &base.Repository{
Name: "test_repo",
- Owner: "forgejo",
- Description: "Exclusively used for testing Github->Forgejo migration",
- CloneURL: server.URL + "/forgejo/test_repo.git",
- OriginalURL: server.URL + "/forgejo/test_repo",
- DefaultBranch: "main",
+ Owner: "go-gitea",
+ Description: "Test repository for testing migration from github to gitea",
+ CloneURL: server.URL + "/go-gitea/test_repo.git",
+ OriginalURL: server.URL + "/go-gitea/test_repo",
+ DefaultBranch: "master",
Website: "https://codeberg.org/forgejo/forgejo/",
}, repo)
topics, err := downloader.GetTopics()
require.NoError(t, err)
- assert.Contains(t, topics, "forgejo")
+ assert.Contains(t, topics, "gitea")
milestones, err := downloader.GetMilestones()
require.NoError(t, err)
assertMilestonesEqual(t, []*base.Milestone{
{
Title: "1.0.0",
- Description: "Version 1",
- Created: time.Date(2025, 8, 7, 12, 48, 56, 0, time.UTC),
- Updated: timePtr(time.Date(2025, time.August, 12, 12, 34, 20, 0, time.UTC)),
- State: "open",
- },
- {
- Title: "0.9.0",
- Description: "A milestone",
- Deadline: timePtr(time.Date(2025, 8, 1, 7, 0, 0, 0, time.UTC)),
- Created: time.Date(2025, 8, 7, 12, 54, 20, 0, time.UTC),
- Updated: timePtr(time.Date(2025, 8, 12, 11, 29, 52, 0, time.UTC)),
- Closed: timePtr(time.Date(2025, 8, 7, 12, 54, 38, 0, time.UTC)),
+ Description: "Milestone 1.0.0",
+ Deadline: timePtr(time.Date(2019, 11, 11, 8, 0, 0, 0, time.UTC)),
+ Created: time.Date(2019, 11, 12, 19, 37, 8, 0, time.UTC),
+ Updated: timePtr(time.Date(2019, 11, 12, 21, 56, 17, 0, time.UTC)),
+ Closed: timePtr(time.Date(2019, 11, 12, 19, 45, 49, 0, time.UTC)),
State: "closed",
},
{
Title: "1.1.0",
- Description: "We can do that",
- Deadline: timePtr(time.Date(2025, 8, 31, 7, 0, 0, 0, time.UTC)),
- Created: time.Date(2025, 8, 7, 12, 50, 58, 0, time.UTC),
- Updated: timePtr(time.Date(2025, 8, 7, 12, 53, 15, 0, time.UTC)),
- State: "open",
+ Description: "Milestone 1.1.0",
+ Deadline: timePtr(time.Date(2019, 11, 12, 8, 0, 0, 0, time.UTC)),
+ Created: time.Date(2019, 11, 12, 19, 37, 25, 0, time.UTC),
+ Updated: timePtr(time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC)),
+ Closed: timePtr(time.Date(2019, 11, 12, 19, 45, 46, 0, time.UTC)),
+ State: "closed",
},
}, milestones)
@@ -204,34 +117,18 @@ func TestGitHubDownloadRepo(t *testing.T) {
},
}, labels)
- id := int64(280443629)
- ct := "application/pdf"
- size := 550175
- dc := 0
-
releases, err := downloader.GetReleases()
require.NoError(t, err)
assertReleasesEqual(t, []*base.Release{
{
- TagName: "v1.0",
- TargetCommitish: "main",
+ TagName: "v0.9.99",
+ TargetCommitish: "master",
Name: "First Release",
- Body: "Hi, this is the first release! The asset contains the wireguard whitepaper, amazing read for such a simple protocol.",
- Created: time.Date(2025, time.August, 7, 13, 2, 19, 0, time.UTC),
- Published: time.Date(2025, time.August, 7, 13, 7, 49, 0, time.UTC),
- PublisherID: 25481501,
- PublisherName: "Gusted",
- Assets: []*base.ReleaseAsset{
- {
- ID: id,
- Name: "wireguard.pdf",
- ContentType: &ct,
- Size: &size,
- DownloadCount: &dc,
- Created: time.Date(2025, time.August, 7, 23, 39, 27, 0, time.UTC),
- Updated: time.Date(2025, time.August, 7, 23, 39, 29, 0, time.UTC),
- },
- },
+ Body: "A test release",
+ Created: time.Date(2019, 11, 9, 16, 49, 21, 0, time.UTC),
+ Published: time.Date(2019, 11, 12, 20, 12, 10, 0, time.UTC),
+ PublisherID: 1669571,
+ PublisherName: "mrsdizzie",
},
}, releases)
@@ -242,41 +139,85 @@ func TestGitHubDownloadRepo(t *testing.T) {
assertIssuesEqual(t, []*base.Issue{
{
Number: 1,
- Title: "First issue",
- Content: "This is an issue.",
- PosterID: 37243484,
- PosterName: "PatDyn",
- State: "open",
- Created: time.Date(2025, time.August, 7, 12, 44, 7, 0, time.UTC),
- Updated: time.Date(2025, time.August, 7, 12, 44, 47, 0, time.UTC),
- },
- {
- Number: 2,
- Title: "Second Issue",
- Content: "Mentioning #1 ",
- Milestone: "1.1.0",
- PosterID: 37243484,
- PosterName: "PatDyn",
- State: "open",
- Created: time.Date(2025, 8, 7, 12, 45, 44, 0, time.UTC),
- Updated: time.Date(2025, 8, 7, 13, 7, 25, 0, time.UTC),
+ Title: "Please add an animated gif icon to the merge button",
+ Content: "I just want the merge button to hurt my eyes a little. \xF0\x9F\x98\x9D ",
+ Milestone: "1.0.0",
+ PosterID: 18600385,
+ PosterName: "guillep2k",
+ State: "closed",
+ Created: time.Date(2019, 11, 9, 17, 0, 29, 0, time.UTC),
+ Updated: time.Date(2019, 11, 12, 20, 29, 53, 0, time.UTC),
Labels: []*base.Label{
{
- Name: "duplicate",
- Color: "cfd3d7",
- Description: "This issue or pull request already exists",
+ Name: "bug",
+ Color: "d73a4a",
+ Description: "Something isn't working",
},
{
Name: "good first issue",
Color: "7057ff",
Description: "Good for newcomers",
},
+ },
+ Reactions: []*base.Reaction{
{
- Name: "help wanted",
- Color: "008672",
- Description: "Extra attention is needed",
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "+1",
},
},
+ Closed: timePtr(time.Date(2019, 11, 12, 20, 22, 22, 0, time.UTC)),
+ },
+ {
+ Number: 2,
+ Title: "Test issue",
+ Content: "This is test issue 2, do not touch!",
+ Milestone: "1.1.0",
+ PosterID: 1669571,
+ PosterName: "mrsdizzie",
+ State: "closed",
+ Created: time.Date(2019, 11, 12, 21, 0, 6, 0, time.UTC),
+ Updated: time.Date(2019, 11, 12, 22, 7, 14, 0, time.UTC),
+ Labels: []*base.Label{
+ {
+ Name: "duplicate",
+ Color: "cfd3d7",
+ Description: "This issue or pull request already exists",
+ },
+ },
+ Reactions: []*base.Reaction{
+ {
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "heart",
+ },
+ {
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "laugh",
+ },
+ {
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "-1",
+ },
+ {
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "confused",
+ },
+ {
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "hooray",
+ },
+ {
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "+1",
+ },
+ },
+ Closed: timePtr(time.Date(2019, 11, 12, 21, 1, 31, 0, time.UTC)),
},
}, issues)
@@ -286,11 +227,26 @@ func TestGitHubDownloadRepo(t *testing.T) {
assertCommentsEqual(t, []*base.Comment{
{
IssueIndex: 2,
- PosterID: 37243484,
- PosterName: "PatDyn",
- Created: time.Date(2025, time.August, 7, 13, 7, 25, 0, time.UTC),
- Updated: time.Date(2025, time.August, 7, 13, 7, 25, 0, time.UTC),
- Content: "Mentioning #3 \nWith some **bold** *statement*",
+ PosterID: 1669571,
+ PosterName: "mrsdizzie",
+ Created: time.Date(2019, 11, 12, 21, 0, 13, 0, time.UTC),
+ Updated: time.Date(2019, 11, 12, 21, 0, 13, 0, time.UTC),
+ Content: "This is a comment",
+ Reactions: []*base.Reaction{
+ {
+ UserID: 1669571,
+ UserName: "mrsdizzie",
+ Content: "+1",
+ },
+ },
+ },
+ {
+ IssueIndex: 2,
+ PosterID: 1669571,
+ PosterName: "mrsdizzie",
+ Created: time.Date(2019, 11, 12, 22, 7, 14, 0, time.UTC),
+ Updated: time.Date(2019, 11, 12, 22, 7, 14, 0, time.UTC),
+ Content: "A second comment",
Reactions: nil,
},
}, comments)
@@ -301,50 +257,52 @@ func TestGitHubDownloadRepo(t *testing.T) {
assertPullRequestsEqual(t, []*base.PullRequest{
{
Number: 3,
- Title: "Update readme.md",
- Content: "Added a feature description",
- Milestone: "1.0.0",
- PosterID: 37243484,
- PosterName: "PatDyn",
- State: "open",
- Created: time.Date(2025, time.August, 7, 12, 47, 6, 0, time.UTC),
- Updated: time.Date(2025, time.August, 12, 13, 16, 49, 0, time.UTC),
+ Title: "Update README.md",
+ Content: "add warning to readme",
+ Milestone: "1.1.0",
+ PosterID: 1669571,
+ PosterName: "mrsdizzie",
+ State: "closed",
+ Created: time.Date(2019, 11, 12, 21, 21, 43, 0, time.UTC),
+ Updated: time.Date(2019, 11, 12, 21, 39, 28, 0, time.UTC),
Labels: []*base.Label{
{
- Name: "enhancement",
- Color: "a2eeef",
- Description: "New feature or request",
+ Name: "documentation",
+ Color: "0075ca",
+ Description: "Improvements or additions to documentation",
},
},
- PatchURL: server.URL + "/forgejo/test_repo/pull/3.patch",
+ PatchURL: server.URL + "/go-gitea/test_repo/pull/3.patch",
Head: base.PullRequestBranch{
- Ref: "some-feature",
- CloneURL: server.URL + "/forgejo/test_repo.git",
- SHA: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
+ Ref: "master",
+ CloneURL: server.URL + "/mrsdizzie/test_repo.git",
+ SHA: "076160cf0b039f13e5eff19619932d181269414b",
RepoName: "test_repo",
- OwnerName: "forgejo",
+ OwnerName: "mrsdizzie",
},
Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "442d28a55b842472c95bead51a4c61f209ac1636",
- OwnerName: "forgejo",
+ Ref: "master",
+ SHA: "72866af952e98d02a73003501836074b286a78f6",
+ OwnerName: "go-gitea",
RepoName: "test_repo",
},
- ForeignIndex: 3,
+ Closed: timePtr(time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC)),
+ Merged: true,
+ MergedTime: timePtr(time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC)),
+ MergeCommitSHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
+ ForeignIndex: 3,
},
{
- Number: 7,
- Title: "Update readme.md",
- Content: "Adding some text to the readme",
+ Number: 4,
+ Title: "Test branch",
+ Content: "do not merge this PR",
Milestone: "1.0.0",
- PosterID: 37243484,
- PosterName: "PatDyn",
- State: "closed",
- Created: time.Date(2025, time.August, 7, 13, 1, 36, 0, time.UTC),
- Updated: time.Date(2025, time.August, 12, 12, 47, 35, 0, time.UTC),
- Closed: timePtr(time.Date(2025, time.August, 7, 13, 2, 19, 0, time.UTC)),
- MergedTime: timePtr(time.Date(2025, time.August, 7, 13, 2, 19, 0, time.UTC)),
+ PosterID: 1669571,
+ PosterName: "mrsdizzie",
+ State: "open",
+ Created: time.Date(2019, 11, 12, 21, 54, 18, 0, time.UTC),
+ Updated: time.Date(2020, 1, 4, 11, 30, 1, 0, time.UTC),
Labels: []*base.Label{
{
Name: "bug",
@@ -352,23 +310,35 @@ func TestGitHubDownloadRepo(t *testing.T) {
Description: "Something isn't working",
},
},
- PatchURL: server.URL + "/forgejo/test_repo/pull/7.patch",
+ PatchURL: server.URL + "/go-gitea/test_repo/pull/4.patch",
Head: base.PullRequestBranch{
- Ref: "another-feature",
- SHA: "5638cb8f3278e467fc1eefcac14d3c0d5d91601f",
+ Ref: "test-branch",
+ SHA: "2be9101c543658591222acbee3eb799edfc3853d",
RepoName: "test_repo",
- OwnerName: "forgejo",
- CloneURL: server.URL + "/forgejo/test_repo.git",
+ OwnerName: "mrsdizzie",
+ CloneURL: server.URL + "/mrsdizzie/test_repo.git",
},
Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "6dd0c6801ddbb7333787e73e99581279492ff449",
- OwnerName: "forgejo",
+ Ref: "master",
+ SHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
+ OwnerName: "go-gitea",
RepoName: "test_repo",
},
- Merged: true,
- MergeCommitSHA: "ca43b48ca2c461f9a5cb66500a154b23d07c9f90",
- ForeignIndex: 7,
+ Merged: false,
+ MergeCommitSHA: "565d1208f5fffdc1c5ae1a2436491eb9a5e4ebae",
+ Reactions: []*base.Reaction{
+ {
+ UserID: 81045,
+ UserName: "lunny",
+ Content: "heart",
+ },
+ {
+ UserID: 81045,
+ UserName: "lunny",
+ Content: "+1",
+ },
+ },
+ ForeignIndex: 4,
},
}, prs)
@@ -376,85 +346,90 @@ func TestGitHubDownloadRepo(t *testing.T) {
require.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
- ID: 3096999684,
+ ID: 315859956,
IssueIndex: 3,
- ReviewerID: 37243484,
- ReviewerName: "PatDyn",
- CommitID: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
- CreatedAt: time.Date(2025, 8, 7, 12, 47, 55, 0, time.UTC),
- State: base.ReviewStateCommented,
+ ReviewerID: 42128690,
+ ReviewerName: "jolheiser",
+ CommitID: "076160cf0b039f13e5eff19619932d181269414b",
+ CreatedAt: time.Date(2019, 11, 12, 21, 35, 24, 0, time.UTC),
+ State: base.ReviewStateApproved,
+ },
+ {
+ ID: 315860062,
+ IssueIndex: 3,
+ ReviewerID: 1824502,
+ ReviewerName: "zeripath",
+ CommitID: "076160cf0b039f13e5eff19619932d181269414b",
+ CreatedAt: time.Date(2019, 11, 12, 21, 35, 36, 0, time.UTC),
+ State: base.ReviewStateApproved,
+ },
+ {
+ ID: 315861440,
+ IssueIndex: 3,
+ ReviewerID: 165205,
+ ReviewerName: "lafriks",
+ CommitID: "076160cf0b039f13e5eff19619932d181269414b",
+ CreatedAt: time.Date(2019, 11, 12, 21, 38, 0, 0, time.UTC),
+ State: base.ReviewStateApproved,
+ },
+ }, reviews)
+
+ reviews, err = downloader.GetReviews(&base.PullRequest{Number: 4, ForeignIndex: 4})
+ require.NoError(t, err)
+ assertReviewsEqual(t, []*base.Review{
+ {
+ ID: 338338740,
+ IssueIndex: 4,
+ ReviewerID: 81045,
+ ReviewerName: "lunny",
+ CommitID: "2be9101c543658591222acbee3eb799edfc3853d",
+ CreatedAt: time.Date(2020, 1, 4, 5, 33, 18, 0, time.UTC),
+ State: base.ReviewStateApproved,
Comments: []*base.ReviewComment{
{
- ID: 2260216729,
- InReplyTo: 0,
- Content: "May want to write more",
- TreePath: "readme.md",
- DiffHunk: "@@ -1,3 +1,5 @@\n # Forgejo Test Repo\n \n This repo is used to test migrations\n+\n+Add some feature description.",
- Position: 5,
- Line: 0,
- CommitID: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
- PosterID: 37243484,
- CreatedAt: time.Date(2025, 8, 7, 12, 47, 50, 0, time.UTC),
- UpdatedAt: time.Date(2025, 8, 7, 12, 47, 55, 0, time.UTC),
+ ID: 363017488,
+ Content: "This is a good pull request.",
+ TreePath: "README.md",
+ DiffHunk: "@@ -1,2 +1,4 @@\n # test_repo\n Test repository for testing migration from github to gitea\n+",
+ Position: 3,
+ CommitID: "2be9101c543658591222acbee3eb799edfc3853d",
+ PosterID: 81045,
+ CreatedAt: time.Date(2020, 1, 4, 5, 33, 6, 0, time.UTC),
+ UpdatedAt: time.Date(2020, 1, 4, 5, 33, 18, 0, time.UTC),
},
},
},
{
- ID: 3097007243,
- IssueIndex: 3,
- ReviewerID: 37243484,
- ReviewerName: "PatDyn",
- CommitID: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
- CreatedAt: time.Date(2025, 8, 7, 12, 49, 36, 0, time.UTC),
+ ID: 338339651,
+ IssueIndex: 4,
+ ReviewerID: 81045,
+ ReviewerName: "lunny",
+ CommitID: "2be9101c543658591222acbee3eb799edfc3853d",
+ CreatedAt: time.Date(2020, 1, 4, 6, 7, 6, 0, time.UTC),
+ State: base.ReviewStateChangesRequested,
+ Content: "Don't add more reviews",
+ },
+ {
+ ID: 338349019,
+ IssueIndex: 4,
+ ReviewerID: 81045,
+ ReviewerName: "lunny",
+ CommitID: "2be9101c543658591222acbee3eb799edfc3853d",
+ CreatedAt: time.Date(2020, 1, 4, 11, 21, 41, 0, time.UTC),
State: base.ReviewStateCommented,
Comments: []*base.ReviewComment{
{
- ID: 2260221159,
- InReplyTo: 0,
- Content: "Comment",
- TreePath: "readme.md",
- DiffHunk: "@@ -1,3 +1,5 @@\n # Forgejo Test Repo\n \n This repo is used to test migrations",
- Position: 3,
- Line: 0,
- CommitID: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
- PosterID: 37243484,
- CreatedAt: time.Date(2025, 8, 7, 12, 49, 36, 0, time.UTC),
- UpdatedAt: time.Date(2025, 8, 7, 12, 49, 36, 0, time.UTC),
+ ID: 363029944,
+ Content: "test a single comment.",
+ TreePath: "LICENSE",
+ DiffHunk: "@@ -19,3 +19,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE.\n+",
+ Position: 4,
+ CommitID: "2be9101c543658591222acbee3eb799edfc3853d",
+ PosterID: 81045,
+ CreatedAt: time.Date(2020, 1, 4, 11, 21, 41, 0, time.UTC),
+ UpdatedAt: time.Date(2020, 1, 4, 11, 21, 41, 0, time.UTC),
},
},
},
}, reviews)
}
-
-func TestGithubMultiToken(t *testing.T) {
- testCases := []struct {
- desc string
- token string
- expectedCloneURL string
- }{
- {
- desc: "Single Token",
- token: "single_token",
- expectedCloneURL: "https://oauth2:single_token@github.com",
- },
- {
- desc: "Multi Token",
- token: "token1,token2",
- expectedCloneURL: "https://oauth2:token1@github.com",
- },
- }
- factory := GithubDownloaderV3Factory{}
-
- for _, tC := range testCases {
- t.Run(tC.desc, func(t *testing.T) {
- opts := base.MigrateOptions{CloneAddr: "https://github.com/go-gitea/gitea", AuthToken: tC.token}
- client, err := factory.New(t.Context(), opts)
- require.NoError(t, err)
-
- cloneURL, err := client.FormatCloneURL(opts, "https://github.com")
- require.NoError(t, err)
-
- assert.Equal(t, tC.expectedCloneURL, cloneURL)
- })
- }
-}
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index f54f682c47..ac0d3bcf7a 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -99,7 +99,6 @@ func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, passw
// Only use basic auth if token is blank and password is NOT
// Basic auth will fail with empty strings, but empty token will allow anonymous public API usage
if token == "" && password != "" {
- //nolint // SA1019 gitlab.NewBasicAuthClient is deprecated: GitLab recommends against using this authentication method
gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
}
@@ -214,7 +213,7 @@ func (g *GitlabDownloader) GetTopics() ([]string, error) {
if err != nil {
return nil, err
}
- return gr.Topics, err
+ return gr.TagList, err
}
// GetMilestones returns milestones
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 30b24f09e8..924dab5144 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -49,7 +49,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
topics, err := downloader.GetTopics()
require.NoError(t, err)
assert.Len(t, topics, 2)
- assert.Equal(t, []string{"migration", "test"}, topics)
+ assert.EqualValues(t, []string{"migration", "test"}, topics)
milestones, err := downloader.GetMilestones()
require.NoError(t, err)
@@ -352,7 +352,7 @@ func TestGitlabSkippedIssueNumber(t *testing.T) {
// the only issue in this repository has number 2
assert.Len(t, issues, 1)
assert.EqualValues(t, 2, issues[0].Number)
- assert.Equal(t, "vpn unlimited errors", issues[0].Title)
+ assert.EqualValues(t, "vpn unlimited errors", issues[0].Title)
prs, _, err := downloader.GetPullRequests(1, 10)
require.NoError(t, err)
@@ -361,7 +361,7 @@ func TestGitlabSkippedIssueNumber(t *testing.T) {
// pull request 3 in Forgejo
assert.Len(t, prs, 1)
assert.EqualValues(t, 3, prs[0].Number)
- assert.Equal(t, "Review", prs[0].Title)
+ assert.EqualValues(t, "Review", prs[0].Title)
}
func gitlabClientMockSetup(t *testing.T) (*http.ServeMux, *httptest.Server, *gitlab.Client) {
@@ -531,7 +531,7 @@ func TestAwardsToReactions(t *testing.T) {
require.NoError(t, json.Unmarshal([]byte(testResponse), &awards))
reactions := downloader.awardsToReactions(awards)
- assert.Equal(t, []*base.Reaction{
+ assert.EqualValues(t, []*base.Reaction{
{
UserName: "lafriks",
UserID: 1241334,
@@ -623,7 +623,7 @@ func TestNoteToComment(t *testing.T) {
for i, note := range notes {
actualComment := *downloader.convertNoteToComment(17, ¬e)
- assert.Equal(t, actualComment, comments[i])
+ assert.EqualValues(t, actualComment, comments[i])
}
}
diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go
index bf0d063ca4..7d7f10c2b9 100644
--- a/services/migrations/gogs_test.go
+++ b/services/migrations/gogs_test.go
@@ -25,7 +25,7 @@ func TestGogsDownloadRepo(t *testing.T) {
resp, err := http.Get("https://try.gogs.io/lunnytest/TESTREPO")
if err != nil || resp.StatusCode/100 != 2 {
// skip and don't run test
- t.Skip("visit test repo failed, ignored")
+ t.Skipf("visit test repo failed, ignored")
return
}
@@ -215,9 +215,9 @@ func TestGogsDownloaderFactory_New(t *testing.T) {
}
assert.IsType(t, &GogsDownloader{}, got)
- assert.Equal(t, tt.baseURL, got.(*GogsDownloader).baseURL)
- assert.Equal(t, tt.repoOwner, got.(*GogsDownloader).repoOwner)
- assert.Equal(t, tt.repoName, got.(*GogsDownloader).repoName)
+ assert.EqualValues(t, tt.baseURL, got.(*GogsDownloader).baseURL)
+ assert.EqualValues(t, tt.repoOwner, got.(*GogsDownloader).repoOwner)
+ assert.EqualValues(t, tt.repoName, got.(*GogsDownloader).repoName)
})
}
}
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index 61630d9c6d..81d1c203fe 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -6,7 +6,6 @@ package migrations
import (
"context"
- "errors"
"fmt"
"net"
"net/url"
@@ -228,7 +227,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
if cloneURL.Scheme == "file" || cloneURL.Scheme == "" {
if cloneAddrURL.Scheme != "file" && cloneAddrURL.Scheme != "" {
- return errors.New("repo info has changed from external to local filesystem")
+ return fmt.Errorf("repo info has changed from external to local filesystem")
}
}
diff --git a/services/migrations/pagure.go b/services/migrations/pagure.go
deleted file mode 100644
index f9433671c0..0000000000
--- a/services/migrations/pagure.go
+++ /dev/null
@@ -1,600 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package migrations
-
-import (
- "context"
- "fmt"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "time"
-
- "forgejo.org/modules/json"
- "forgejo.org/modules/log"
- base "forgejo.org/modules/migration"
- "forgejo.org/modules/proxy"
- "forgejo.org/modules/structs"
- "forgejo.org/modules/util"
-)
-
-var (
- _ base.Downloader = &PagureDownloader{}
- _ base.DownloaderFactory = &PagureDownloaderFactory{}
-)
-
-func init() {
- RegisterDownloaderFactory(&PagureDownloaderFactory{})
-}
-
-// PagureDownloaderFactory defines a downloader factory
-type PagureDownloaderFactory struct{}
-
-// PagureUser defines a user on Pagure to be migrated over to Forgejo
-type PagureUser struct {
- FullURL string `json:"full_url"`
- Fullname string `json:"fullname"`
- Name string `json:"name"`
- URLPath string `json:"url_path"`
-}
-
-// PagureRepoInfo describes the repository with preliminary information
-type PagureRepoInfo struct {
- ID int64 `json:"id"`
- Name string `json:"name"`
- FullName string `json:"fullname"`
- Description string `json:"description"`
- Topics []string `json:"tags"`
- CloseStatuses []string `json:"close_status"`
- Priorities map[string]string `json:"priorities"`
- Milestones map[string]struct {
- Active bool `json:"active"`
- Date *string `json:"date"`
- } `json:"milestones"`
-}
-
-// PagureDownloader implements a Downloader interface to get repository information from Pagure
-type PagureDownloader struct {
- base.NullDownloader
- ctx context.Context
- client *http.Client
- baseURL *url.URL
- meta PagureRepoInfo
- repoName string
- token string
- privateIssuesOnlyRepo bool
- repoID int64
- maxIssueIndex int64
- userMap map[string]*PagureUser
- milestoneMap map[int64]string
- priorities map[string]string
-}
-
-// PagureLabelsList defines a list of labels under an issue tracker
-type PagureLabelsList struct {
- Labels []string `json:"tags"`
-}
-
-// PagureLabel defines a label under the issue tracker labels list
-type PagureLabel struct {
- Label string `json:"tag"`
- LabelColor string `json:"tag_color"`
- LabelDescription string `json:"tag_description"`
-}
-
-// PagureIssueContext confirms if a said unit is an issue ticket or a pull request
-type PagureIssueContext struct {
- IsPullRequest bool
-}
-
-// PagureIssuesResponse describes a list of issue tickets under an issue tracker
-type PagureIssuesResponse struct {
- Issues []struct {
- Assignee any `json:"assignee"`
- Blocks []string `json:"blocks"`
- CloseStatus string `json:"close_status"`
- ClosedAt string `json:"closed_at"`
- ClosedBy any `json:"closed_by"`
- Comments []any `json:"comments"`
- Content string `json:"content"`
- CustomFields []any `json:"custom_fields"`
- DateCreated string `json:"date_created"`
- Depends []any `json:"depends"`
- ID int64 `json:"id"`
- LastUpdated string `json:"last_updated"`
- Milestone string `json:"milestone"`
- Priority int64 `json:"priority"`
- Private bool `json:"private"`
- Status string `json:"status"`
- Tags []string `json:"tags"`
- Title string `json:"title"`
- User PagureUser `json:"user"`
- } `json:"issues"`
- Pagination struct {
- First string `json:"first"`
- Last string `json:"last"`
- Next *string `json:"next"`
- Page int `json:"page"`
- Pages int `json:"pages"`
- PerPage int `json:"per_page"`
- Prev string `json:"prev"`
- }
-}
-
-// PagureIssueDetail describes a list of issue comments under an issue ticket
-type PagureIssueDetail struct {
- Comment string `json:"comment"`
- DateCreated string `json:"date_created"`
- ID int64 `json:"id"`
- Notification bool `json:"notification"`
- User PagureUser `json:"user"`
-}
-
-// PagureCommitInfo describes a commit
-type PagureCommitInfo struct {
- Author string `json:"author"`
- CommitTime int64 `json:"commit_time"`
- CommitTimeOffset int `json:"commit_time_offset"`
- Committer string `json:"committer"`
- Hash string `json:"hash"`
- Message string `json:"message"`
- ParentIDs []string `json:"parent_ids"`
- TreeID string `json:"tree_id"`
-}
-
-// PagurePRRresponse describes a list of pull requests under an issue tracker
-type PagurePRResponse struct {
- Pagination struct {
- Next *string `json:"next"`
- } `json:"pagination"`
- Requests []struct {
- Branch string `json:"branch"`
- BranchFrom string `json:"branch_from"`
- CommitStop string `json:"commit_stop"`
- DateCreated string `json:"date_created"`
- FullURL string `json:"full_url"`
- ID int `json:"id"`
- InitialComment string `json:"initial_comment"`
- LastUpdated string `json:"last_updated"`
- Status string `json:"status"`
- Tags []string `json:"tags"`
- Title string `json:"title"`
- User PagureUser `json:"user"`
- ClosedAt string `json:"closed_at"`
- ClosedBy PagureUser `json:"closed_by"`
- RepoFrom struct {
- FullURL string `json:"full_url"`
- } `json:"repo_from"`
- } `json:"requests"`
-}
-
-// processDate converts epoch time string to Go formatted time
-func processDate(dateStr *string) time.Time {
- date := time.Time{}
- if dateStr == nil || *dateStr == "" {
- return date
- }
-
- unix, err := strconv.Atoi(*dateStr)
- if err != nil {
- log.Error("Error:", err)
- return date
- }
-
- date = time.Unix(int64(unix), 0)
- return date
-}
-
-// New returns a downloader related to this factory according MigrateOptions
-func (f *PagureDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
- u, err := url.Parse(opts.CloneAddr)
- if err != nil {
- return nil, err
- }
-
- var repoName string
-
- fields := strings.Split(strings.Trim(u.Path, "/"), "/")
- if len(fields) == 2 {
- repoName = fields[0] + "/" + strings.TrimSuffix(fields[1], ".git")
- } else if len(fields) == 1 {
- repoName = strings.TrimSuffix(fields[0], ".git")
- } else {
- return nil, fmt.Errorf("invalid path: %s", u.Path)
- }
-
- u.Path, u.Fragment = "", ""
- log.Info("Create Pagure downloader. BaseURL: %v RepoName: %s", u, repoName)
-
- return NewPagureDownloader(ctx, u, opts.AuthToken, repoName), nil
-}
-
-// GitServiceType returns the type of Git service
-func (f *PagureDownloaderFactory) GitServiceType() structs.GitServiceType {
- return structs.PagureService
-}
-
-// SetContext sets context
-func (d *PagureDownloader) SetContext(ctx context.Context) {
- d.ctx = ctx
-}
-
-// NewPagureDownloader creates a new downloader object
-func NewPagureDownloader(ctx context.Context, baseURL *url.URL, token, repoName string) *PagureDownloader {
- var privateIssuesOnlyRepo bool
- if token != "" {
- privateIssuesOnlyRepo = true
- }
-
- downloader := &PagureDownloader{
- ctx: ctx,
- baseURL: baseURL,
- repoName: repoName,
- client: &http.Client{
- Transport: &http.Transport{
- Proxy: proxy.Proxy(),
- },
- },
- token: token,
- privateIssuesOnlyRepo: privateIssuesOnlyRepo,
- userMap: make(map[string]*PagureUser),
- milestoneMap: make(map[int64]string),
- priorities: make(map[string]string),
- }
-
- return downloader
-}
-
-// String sets the default text for information purposes
-func (d *PagureDownloader) String() string {
- return fmt.Sprintf("migration from Pagure server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
-}
-
-// LogString sets the default text for logging purposes
-func (d *PagureDownloader) LogString() string {
- if d == nil {
- return ""
- }
- return fmt.Sprintf("", d.baseURL, d.repoID, d.repoName)
-}
-
-// callAPI handles all the requests made against Pagure
-func (d *PagureDownloader) callAPI(endpoint string, parameter map[string]string, result any) error {
- u, err := d.baseURL.Parse(endpoint)
- if err != nil {
- return err
- }
-
- if parameter != nil {
- query := u.Query()
- for k, v := range parameter {
- query.Set(k, v)
- }
- u.RawQuery = query.Encode()
- }
-
- req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil)
- if err != nil {
- return err
- }
- if d.privateIssuesOnlyRepo {
- req.Header.Set("Authorization", "token "+d.token)
- }
-
- resp, err := d.client.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- decoder := json.NewDecoder(resp.Body)
- return decoder.Decode(&result)
-}
-
-// GetRepoInfo returns repository information from Pagure
-func (d *PagureDownloader) GetRepoInfo() (*base.Repository, error) {
- err := d.callAPI("/api/0/"+d.repoName, nil, &d.meta)
- if err != nil {
- return nil, err
- }
-
- d.repoID, d.priorities = d.meta.ID, d.meta.Priorities
-
- cloneURL, err := d.baseURL.Parse(d.meta.FullName)
- if err != nil {
- return nil, err
- }
- originalURL, err := d.baseURL.Parse(d.meta.FullName)
- if err != nil {
- return nil, err
- }
-
- return &base.Repository{
- Name: d.meta.Name,
- Description: d.meta.Description,
- CloneURL: cloneURL.String() + ".git",
- OriginalURL: originalURL.String(),
- }, nil
-}
-
-// GetMilestones returns milestones information from Pagure
-func (d *PagureDownloader) GetMilestones() ([]*base.Milestone, error) {
- milestones := make([]*base.Milestone, 0, len(d.meta.Milestones))
- for name, details := range d.meta.Milestones {
- state := "closed"
- if details.Active {
- state = "open"
- }
-
- deadline := processDate(details.Date)
- milestones = append(milestones, &base.Milestone{
- Title: name,
- Description: "",
- Deadline: &deadline,
- State: state,
- })
- }
-
- return milestones, nil
-}
-
-// GetLabels returns labels information from Pagure
-func (d *PagureDownloader) GetLabels() ([]*base.Label, error) {
- rawLabels := PagureLabelsList{}
-
- err := d.callAPI("/api/0/"+d.repoName+"/tags", nil, &rawLabels)
- if err != nil {
- return nil, err
- }
-
- labels := make([]*base.Label, 0, len(rawLabels.Labels)+len(d.meta.CloseStatuses))
-
- for _, label := range rawLabels.Labels {
- rawLabel := PagureLabel{}
- err = d.callAPI("/api/0/"+d.repoName+"/tag/"+label, nil, &rawLabel)
- if err != nil {
- return nil, err
- }
- labels = append(labels, &base.Label{
- Name: label,
- Description: rawLabel.LabelDescription,
- Color: strings.TrimPrefix(rawLabel.LabelColor, "#"),
- })
- }
-
- for _, closeStatus := range d.meta.CloseStatuses {
- labels = append(labels, &base.Label{
- Name: "Closed As/" + closeStatus,
- Description: "Closed with the reason of " + closeStatus,
- Color: "FF0000",
- Exclusive: true,
- })
- }
-
- for _, value := range d.priorities {
- if value != "" {
- labels = append(labels, &base.Label{
- Name: "Priority/" + value,
- Description: "Priority of " + value,
- Color: "FF00FF",
- Exclusive: true,
- })
- }
- }
-
- return labels, nil
-}
-
-// GetIssues returns issue tickets from Pagure
-func (d *PagureDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
- rawIssues := PagureIssuesResponse{}
-
- err := d.callAPI(
- "/api/0/"+d.repoName+"/issues",
- map[string]string{
- "page": strconv.Itoa(page),
- "per_page": strconv.Itoa(perPage),
- "status": "all",
- },
- &rawIssues,
- )
- if err != nil {
- return nil, false, err
- }
-
- issues := make([]*base.Issue, 0, len(rawIssues.Issues))
- for _, issue := range rawIssues.Issues {
- log.Debug("Processing issue %d", issue.ID)
- if d.privateIssuesOnlyRepo && !issue.Private {
- log.Info("Skipping issue %d because it is not private and we are only downloading private issues", issue.ID)
- continue
- }
- labels := []*base.Label{}
- for _, tag := range issue.Tags {
- labels = append(labels, &base.Label{Name: tag})
- }
-
- if issue.CloseStatus != "" {
- labels = append(labels, &base.Label{Name: "Closed As/" + issue.CloseStatus})
- }
-
- priorityStr := ""
- if issue.Priority != 0 {
- priorityStr = strconv.FormatInt(issue.Priority, 10)
- }
-
- if priorityStr != "" {
- priorityValue, ok := d.priorities[priorityStr]
- if ok {
- labels = append(labels, &base.Label{Name: "Priority/" + priorityValue})
- }
- }
- log.Trace("Adding issue: %d", issue.ID)
-
- closedat := processDate(&issue.ClosedAt)
-
- issues = append(issues, &base.Issue{
- Title: issue.Title,
- Number: issue.ID,
- PosterName: issue.User.Name,
- PosterID: -1,
- Content: issue.Content,
- Milestone: issue.Milestone,
- State: strings.ToLower(issue.Status),
- Created: processDate(&issue.DateCreated),
- Updated: processDate(&issue.LastUpdated),
- Closed: &closedat,
- Labels: labels,
- ForeignIndex: issue.ID,
- Context: PagureIssueContext{IsPullRequest: false},
- })
-
- if d.maxIssueIndex < issue.ID {
- d.maxIssueIndex = issue.ID
- }
- }
- hasNext := rawIssues.Pagination.Next == nil
-
- return issues, hasNext, nil
-}
-
-// GetComments returns issue comments from Pagure
-func (d *PagureDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
- context, ok := commentable.GetContext().(PagureIssueContext)
- if !ok {
- return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
- }
-
- list := struct {
- Comments []PagureIssueDetail `json:"comments"`
- }{}
- var endpoint string
-
- if context.IsPullRequest {
- endpoint = fmt.Sprintf("/api/0/%s/pull-request/%d", d.repoName, commentable.GetForeignIndex())
- } else {
- endpoint = fmt.Sprintf("/api/0/%s/issue/%d", d.repoName, commentable.GetForeignIndex())
- }
-
- err := d.callAPI(endpoint, nil, &list)
- if err != nil {
- log.Error("Error calling API: %v", err)
- return nil, false, err
- }
-
- comments := make([]*base.Comment, 0, len(list.Comments))
- for _, unit := range list.Comments {
- if len(unit.Comment) == 0 {
- log.Error("Empty comment")
- continue
- }
-
- log.Trace("Adding comment: %d", unit.ID)
- c := &base.Comment{
- IssueIndex: commentable.GetLocalIndex(),
- Index: unit.ID,
- PosterName: unit.User.Name,
- PosterID: -1,
- Content: unit.Comment,
- Created: processDate(&unit.DateCreated),
- }
- comments = append(comments, c)
- }
-
- return comments, true, nil
-}
-
-// GetPullRequests returns pull requests from Pagure
-func (d *PagureDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
- // Could not figure out how to disable this in opts, so if a private issues only repo,
- // We just return an empty list
- if d.privateIssuesOnlyRepo {
- pullRequests := make([]*base.PullRequest, 0)
- return pullRequests, true, nil
- }
-
- rawPullRequests := PagurePRResponse{}
- commit := PagureCommitInfo{}
-
- err := d.callAPI(
- "/api/0/"+d.repoName+"/pull-requests",
- map[string]string{
- "page": strconv.Itoa(page),
- "per_page": strconv.Itoa(perPage),
- "status": "all",
- },
- &rawPullRequests,
- )
- if err != nil {
- return nil, false, err
- }
-
- pullRequests := make([]*base.PullRequest, 0, len(rawPullRequests.Requests))
-
- for _, pr := range rawPullRequests.Requests {
- var state, baseSHA string
- var merged bool
- labels := []*base.Label{}
-
- for _, tag := range pr.Tags {
- labels = append(labels, &base.Label{Name: tag})
- }
- mergedtime := processDate(&pr.ClosedAt)
-
- err = d.callAPI("/api/0/"+d.repoName+"/c/"+pr.CommitStop+"/info", nil, &commit)
- if err != nil {
- return nil, false, err
- }
-
- if util.ASCIIEqualFold(pr.Status, "merged") {
- state, merged, baseSHA = "closed", true, commit.ParentIDs[0]
- } else if util.ASCIIEqualFold(pr.Status, "open") {
- state, merged, baseSHA = "open", false, commit.ParentIDs[0]
- } else {
- state, merged, baseSHA = "closed", false, commit.ParentIDs[0]
- }
-
- pullRequests = append(pullRequests, &base.PullRequest{
- Title: pr.Title,
- Number: int64(pr.ID),
- PosterName: pr.User.Name,
- PosterID: -1,
- Content: pr.InitialComment,
- State: state,
- Created: processDate(&pr.DateCreated),
- Updated: processDate(&pr.LastUpdated),
- MergedTime: &mergedtime,
- Closed: &mergedtime,
- Merged: merged,
- Labels: labels,
- Head: base.PullRequestBranch{
- Ref: pr.BranchFrom,
- SHA: pr.CommitStop,
- RepoName: d.repoName,
- CloneURL: pr.RepoFrom.FullURL + ".git",
- },
- Base: base.PullRequestBranch{
- Ref: pr.Branch,
- SHA: baseSHA,
- RepoName: d.repoName,
- },
- ForeignIndex: int64(pr.ID),
- PatchURL: pr.FullURL + ".patch",
- Context: PagureIssueContext{IsPullRequest: true},
- })
-
- // SECURITY: Ensure that the PR is safe
- _ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
- }
-
- hasNext := rawPullRequests.Pagination.Next == nil
-
- return pullRequests, hasNext, nil
-}
-
-// GetTopics return repository topics from Pagure
-func (d *PagureDownloader) GetTopics() ([]string, error) {
- return d.meta.Topics, nil
-}
diff --git a/services/migrations/pagure_test.go b/services/migrations/pagure_test.go
deleted file mode 100644
index 22b2eed85e..0000000000
--- a/services/migrations/pagure_test.go
+++ /dev/null
@@ -1,637 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package migrations
-
-import (
- "net/url"
- "os"
- "strings"
- "testing"
- "time"
-
- "forgejo.org/models/unittest"
- base "forgejo.org/modules/migration"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestPagureDownloadRepoWithPublicIssues(t *testing.T) {
- // Skip tests if Pagure token is not found
- cloneUser := os.Getenv("PAGURE_CLONE_USER")
- clonePassword := os.Getenv("PAGURE_CLONE_PASSWORD")
- apiUser := os.Getenv("PAGURE_API_USER")
- apiPassword := os.Getenv("PAGURE_API_TOKEN")
-
- fixtPath := "./testdata/pagure/full_download/unauthorized"
- server := unittest.NewMockWebServer(t, "https://pagure.io", fixtPath, false)
- defer server.Close()
-
- serverURL, err := url.Parse(server.URL)
- require.NoError(t, err)
-
- if clonePassword != "" || cloneUser != "" {
- serverURL.User = url.UserPassword(cloneUser, clonePassword)
- }
- serverURL.Path = "protop2g-test-srce.git"
- cloneAddr := serverURL.String()
-
- factory := &PagureDownloaderFactory{}
- downloader, err := factory.New(t.Context(), base.MigrateOptions{
- CloneAddr: cloneAddr,
- AuthUsername: apiUser,
- AuthPassword: apiPassword,
- })
- require.NoError(t, err)
-
- repo, err := downloader.GetRepoInfo()
- require.NoError(t, err)
-
- // Testing repository contents migration
- assertRepositoryEqual(t, &base.Repository{
- Name: "protop2g-test-srce",
- Owner: "",
- Description: "The source namespace for the Pagure Exporter project to run tests against",
- CloneURL: cloneAddr,
- OriginalURL: strings.ReplaceAll(cloneAddr, ".git", ""),
- }, repo)
-
- topics, err := downloader.GetTopics()
- require.NoError(t, err)
-
- // Testing repository topics migration
- assert.Equal(t, []string{"srce", "test", "gridhead", "protop2g"}, topics)
-
- // Testing labels migration
- labels, err := downloader.GetLabels()
- require.NoError(t, err)
- assert.Len(t, labels, 15)
-
- // Testing issue tickets probing
- issues, isEnd, err := downloader.GetIssues(1, 20)
- require.NoError(t, err)
- assert.True(t, isEnd)
-
- // Testing issue tickets migration
- assertIssuesEqual(t, []*base.Issue{
- {
- Number: 2,
- Title: "This is the title of the second test issue",
- Content: "This is the body of the second test issue",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "closed",
- Milestone: "Milestone BBBB",
- Created: time.Date(2023, time.October, 13, 4, 1, 16, 0, time.UTC),
- Updated: time.Date(2025, time.June, 25, 6, 25, 57, 0, time.UTC),
- Closed: timePtr(time.Date(2025, time.June, 25, 6, 22, 59, 0, time.UTC)),
- Labels: []*base.Label{
- {
- Name: "cccc",
- },
- {
- Name: "dddd",
- },
- {
- Name: "Closed As/Complete",
- },
- {
- Name: "Priority/Rare",
- },
- },
- },
- {
- Number: 1,
- Title: "This is the title of the first test issue",
- Content: "This is the body of the first test issue",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "open",
- Milestone: "Milestone AAAA",
- Created: time.Date(2023, time.October, 13, 3, 57, 42, 0, time.UTC),
- Updated: time.Date(2025, time.June, 25, 6, 25, 45, 0, time.UTC),
- Closed: timePtr(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)),
- Labels: []*base.Label{
- {
- Name: "aaaa",
- },
- {
- Name: "bbbb",
- },
- {
- Name: "Priority/Epic",
- },
- },
- },
- }, issues)
-
- // Testing comments under issue tickets
- comments, _, err := downloader.GetComments(issues[0])
- require.NoError(t, err)
- assertCommentsEqual(t, []*base.Comment{
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2023, time.October, 13, 4, 3, 30, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue tagged with: cccc, dddd",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2023, time.October, 13, 4, 6, 4, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "The is the first comment under the second test issue",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2023, time.October, 13, 4, 6, 16, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "The is the second comment under the second test issue",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2023, time.October, 13, 4, 7, 12, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue status updated to: Closed (was: Open)",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.May, 8, 4, 50, 21, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone BBBB",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 22, 52, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: None (was: Milestone BBBB)\n- Issue status updated to: Open (was: Closed)",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 23, 0o2, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue close_status updated to: Complete\n- Issue status updated to: Closed (was: Open)",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 24, 34, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone BBBB",
- },
- {
- IssueIndex: 2,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 25, 57, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue priority set to: Rare",
- },
- }, comments)
-
- prs, isEnd, err := downloader.GetPullRequests(1, 20)
- require.NoError(t, err)
- assert.True(t, isEnd)
-
- // Testing pull requests migrated
- assertPullRequestsEqual(t, []*base.PullRequest{
- {
- Number: 10,
- Title: "Change the branch identity to `test-ffff` in the README.md file",
- Content: "Signed-off-by: Akashdeep Dhar ",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "closed",
- Created: time.Date(2025, time.May, 19, 6, 12, 45, 0, time.UTC),
- Updated: time.Date(2025, time.May, 19, 6, 17, 11, 0, time.UTC),
- Closed: timePtr(time.Date(2025, time.May, 19, 6, 17, 11, 0, time.UTC)),
- MergedTime: timePtr(time.Date(2025, time.May, 19, 6, 17, 11, 0, time.UTC)),
- Merged: true,
- PatchURL: server.URL + "/protop2g-test-srce/pull-request/10.patch",
- Labels: []*base.Label{
- {
- Name: "ffff",
- },
- },
- Head: base.PullRequestBranch{
- Ref: "test-ffff",
- SHA: "1a6ccc212aa958a0fe76155c2907c889969a7224",
- RepoName: "protop2g-test-srce",
- CloneURL: server.URL + "/protop2g-test-srce.git",
- },
- Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- RepoName: "protop2g-test-srce",
- },
- },
- {
- Number: 9,
- Title: "Change the branch identity to `test-eeee` in the README.md file",
- Content: "Signed-off-by: Akashdeep Dhar ",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "closed",
- Created: time.Date(2025, time.May, 19, 6, 12, 41, 0, time.UTC),
- Updated: time.Date(2025, time.May, 19, 6, 14, 3, 0, time.UTC),
- Closed: timePtr(time.Date(2025, time.May, 19, 6, 14, 3, 0, time.UTC)),
- MergedTime: timePtr(time.Date(2025, time.May, 19, 6, 14, 3, 0, time.UTC)),
- Merged: true,
- PatchURL: server.URL + "/protop2g-test-srce/pull-request/9.patch",
- Labels: []*base.Label{
- {
- Name: "eeee",
- },
- },
- Head: base.PullRequestBranch{
- Ref: "test-eeee",
- SHA: "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- RepoName: "protop2g-test-srce",
- CloneURL: server.URL + "/protop2g-test-srce.git",
- },
- Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7",
- RepoName: "protop2g-test-srce",
- },
- },
- {
- Number: 8,
- Title: "Change the branch identity to `test-dddd` in the README.md file",
- Content: "Signed-off-by: Akashdeep Dhar ",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "closed",
- Created: time.Date(2025, time.May, 5, 6, 45, 32, 0, time.UTC),
- Updated: time.Date(2025, time.May, 5, 6, 54, 13, 0, time.UTC),
- Closed: timePtr(time.Date(2025, time.May, 5, 6, 54, 13, 0, time.UTC)),
- MergedTime: timePtr(time.Date(2025, time.May, 5, 6, 54, 13, 0, time.UTC)), // THIS IS WRONG
- Merged: false,
- PatchURL: server.URL + "/protop2g-test-srce/pull-request/8.patch",
- Labels: []*base.Label{
- {
- Name: "dddd",
- },
- },
- Head: base.PullRequestBranch{
- Ref: "test-dddd",
- SHA: "0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160",
- RepoName: "protop2g-test-srce",
- CloneURL: server.URL + "/protop2g-test-srce.git",
- },
- Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7",
- RepoName: "protop2g-test-srce",
- },
- },
- {
- Number: 7,
- Title: "Change the branch identity to `test-cccc` in the README.md file",
- Content: "Signed-off-by: Akashdeep Dhar ",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "closed",
- Created: time.Date(2025, time.May, 5, 6, 45, 6, 0, time.UTC),
- Updated: time.Date(2025, time.May, 5, 6, 54, 3, 0, time.UTC), // IT SHOULD BE NIL
- Closed: timePtr(time.Date(2025, time.May, 5, 6, 54, 3, 0, time.UTC)), // IT is CLOSED, Not MERGED so SHOULD NOT BE NIL
- MergedTime: timePtr(time.Date(2025, time.May, 5, 6, 54, 3, 0, time.UTC)), // THIS IS WRONG
- Merged: false,
- PatchURL: server.URL + "/protop2g-test-srce/pull-request/7.patch",
- Labels: []*base.Label{
- {
- Name: "cccc",
- },
- },
- Head: base.PullRequestBranch{
- Ref: "test-cccc",
- SHA: "f1246e331cade9341b9e4f311b7a134f99893d21",
- RepoName: "protop2g-test-srce",
- CloneURL: server.URL + "/protop2g-test-srce.git",
- },
- Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7",
- RepoName: "protop2g-test-srce",
- },
- },
- {
- Number: 6,
- Title: "Change the branch identity to `test-bbbb` in the README.md file",
- Content: "Signed-off-by: Akashdeep Dhar ",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "open",
- Created: time.Date(2025, time.May, 5, 6, 44, 30, 0, time.UTC),
- Updated: time.Date(2025, time.May, 19, 8, 30, 50, 0, time.UTC),
- Closed: timePtr(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)),
- MergedTime: timePtr(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)),
- Merged: false,
- PatchURL: server.URL + "/protop2g-test-srce/pull-request/6.patch",
- Labels: []*base.Label{
- {
- Name: "bbbb",
- },
- },
- Head: base.PullRequestBranch{
- Ref: "test-bbbb",
- SHA: "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- RepoName: "protop2g-test-srce",
- CloneURL: server.URL + "/protop2g-test-srce.git",
- },
- Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7",
- RepoName: "protop2g-test-srce",
- },
- },
- {
- Number: 5,
- Title: "Change the branch identity to `test-aaaa` in the README.md file",
- Content: "Signed-off-by: Akashdeep Dhar ",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "open",
- Created: time.Date(2025, time.May, 5, 6, 43, 57, 0, time.UTC),
- Updated: time.Date(2025, time.May, 19, 6, 29, 45, 0, time.UTC),
- Closed: timePtr(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)),
- MergedTime: timePtr(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)),
- Merged: false,
- PatchURL: server.URL + "/protop2g-test-srce/pull-request/5.patch",
- Labels: []*base.Label{
- {
- Name: "aaaa",
- },
- },
- Head: base.PullRequestBranch{
- Ref: "test-aaaa",
- SHA: "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- RepoName: "protop2g-test-srce",
- CloneURL: server.URL + "/protop2g-test-srce.git",
- },
- Base: base.PullRequestBranch{
- Ref: "main",
- SHA: "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7",
- RepoName: "protop2g-test-srce",
- },
- },
- }, prs)
-
- // Testing comments under pull requests
- comments, _, err = downloader.GetComments(prs[5])
- require.NoError(t, err)
- assertCommentsEqual(t, []*base.Comment{
- {
- IssueIndex: 5,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.May, 5, 6, 44, 13, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: aaaa",
- },
- {
- IssueIndex: 5,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.May, 7, 5, 25, 21, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "This is the first comment under this pull request.",
- },
- {
- IssueIndex: 5,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.May, 7, 5, 25, 29, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "This is the second comment under this pull request.",
- },
- }, comments)
-
- // Testing milestones migration
- milestones, err := downloader.GetMilestones()
- require.NoError(t, err)
- dict := map[string]*base.Milestone{
- "Milestone AAAA": {
- Title: "Milestone AAAA",
- Deadline: timePtr(time.Date(2025, time.December, 12, 0, 0, 0, 0, time.UTC)),
- State: "open",
- },
- "Milestone BBBB": {
- Title: "Milestone BBBB",
- Deadline: timePtr(time.Date(2025, time.December, 12, 0, 0, 0, 0, time.UTC)),
- State: "closed",
- },
- "Milestone CCCC": {
- Title: "Milestone CCCC",
- Deadline: timePtr(time.Date(2025, time.December, 12, 0, 0, 0, 0, time.UTC)),
- State: "open",
- },
- "Milestone DDDD": {
- Title: "Milestone DDDD",
- Deadline: timePtr(time.Date(2025, time.December, 12, 0, 0, 0, 0, time.UTC)),
- State: "closed",
- },
- }
-
- // We do not like when tests fail just because of dissimilar ordering
- for _, item := range milestones {
- assertMilestoneEqual(t, item, dict[item.Title])
- }
-}
-
-func TestPagureDownloadRepoWithPrivateIssues(t *testing.T) {
- t.Skip("Does not work")
- // Skip tests if Pagure token is not found
- cloneUser := os.Getenv("PAGURE_CLONE_USER")
- clonePassword := os.Getenv("PAGURE_CLONE_PASSWORD")
- apiUser := os.Getenv("PAGURE_API_USER")
- apiPassword := os.Getenv("PAGURE_API_TOKEN")
-
- fixtPath := "./testdata/pagure/full_download/authorized"
- server := unittest.NewMockWebServer(t, "https://pagure.io", fixtPath, false)
- defer server.Close()
-
- serverURL, err := url.Parse(server.URL)
- require.NoError(t, err)
-
- if clonePassword != "" || cloneUser != "" {
- serverURL.User = url.UserPassword(cloneUser, clonePassword)
- }
- serverURL.Path = "protop2g-test-srce.git"
- cloneAddr := serverURL.String()
-
- factory := &PagureDownloaderFactory{}
- downloader, err := factory.New(t.Context(), base.MigrateOptions{
- CloneAddr: cloneAddr,
- AuthUsername: apiUser,
- AuthPassword: apiPassword,
- AuthToken: apiPassword,
- })
- require.NoError(t, err)
-
- repo, err := downloader.GetRepoInfo()
- require.NoError(t, err)
-
- // Testing repository contents migration
- assertRepositoryEqual(t, &base.Repository{
- Name: "protop2g-test-srce",
- Owner: "",
- Description: "The source namespace for the Pagure Exporter project to run tests against",
- CloneURL: cloneAddr,
- OriginalURL: strings.ReplaceAll(cloneAddr, ".git", ""),
- }, repo)
-
- topics, err := downloader.GetTopics()
- require.NoError(t, err)
-
- // Testing repository topics migration
- assert.Equal(t, []string{"srce", "test", "gridhead", "protop2g"}, topics)
-
- // Testing labels migration
- labels, err := downloader.GetLabels()
- require.NoError(t, err)
- assert.Len(t, labels, 15)
-
- // Testing issue tickets probing
- issues, isEnd, err := downloader.GetIssues(1, 20)
- require.NoError(t, err)
- assert.True(t, isEnd)
-
- // Testing issue tickets migration
- assertIssuesEqual(t, []*base.Issue{
- {
- Number: 4,
- Title: "This is the title of the fourth test issue",
- Content: "This is the body of the fourth test issue",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "closed",
- Milestone: "Milestone DDDD",
- Created: time.Date(2023, time.November, 21, 8, 6, 56, 0, time.UTC),
- Updated: time.Date(2025, time.June, 25, 6, 26, 26, 0, time.UTC),
- Closed: timePtr(time.Date(2025, time.June, 25, 6, 23, 51, 0, time.UTC)),
- Labels: []*base.Label{
- {
- Name: "gggg",
- },
- {
- Name: "hhhh",
- },
- {
- Name: "Closed As/Baseless",
- },
- {
- Name: "Priority/Common",
- },
- },
- },
- {
- Number: 3,
- Title: "This is the title of the third test issue",
- Content: "This is the body of the third test issue",
- PosterName: "t0xic0der",
- PosterID: -1,
- State: "open",
- Milestone: "Milestone CCCC",
- Created: time.Date(2023, time.November, 21, 8, 3, 57, 0, time.UTC),
- Updated: time.Date(2025, time.June, 25, 6, 26, 7, 0, time.UTC),
- Closed: timePtr(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)),
- Labels: []*base.Label{
- {
- Name: "eeee",
- },
- {
- Name: "ffff",
- },
- {
- Name: "Priority/Uncommon",
- },
- },
- },
- }, issues)
-
- // Testing comments under issue tickets
- comments, _, err := downloader.GetComments(issues[0])
- require.NoError(t, err)
- assertCommentsEqual(t, []*base.Comment{
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2023, time.November, 21, 8, 7, 25, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "This is the first comment under the fourth test issue",
- },
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2023, time.November, 21, 8, 7, 34, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "This is the second comment under the fourth test issue",
- },
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2023, time.November, 21, 8, 8, 1, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue status updated to: Closed (was: Open)",
- },
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.May, 8, 4, 50, 46, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone DDDD",
- },
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 23, 46, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: None (was: Milestone DDDD)\n- Issue status updated to: Open (was: Closed)",
- },
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 23, 52, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue close_status updated to: Baseless\n- Issue status updated to: Closed (was: Open)",
- },
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 24, 55, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone DDDD",
- },
- {
- IssueIndex: 4,
- PosterName: "t0xic0der",
- PosterID: -1,
- Created: time.Date(2025, time.June, 25, 6, 26, 26, 0, time.UTC),
- Updated: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
- Content: "**Metadata Update from @t0xic0der**:\n- Issue priority set to: Common",
- },
- }, comments)
-}
diff --git a/services/migrations/restore.go b/services/migrations/restore.go
index e8a1e0fdcf..fe2628da52 100644
--- a/services/migrations/restore.go
+++ b/services/migrations/restore.go
@@ -12,7 +12,7 @@ import (
base "forgejo.org/modules/migration"
- "go.yaml.in/yaml/v3"
+ "gopkg.in/yaml.v3"
)
// RepositoryRestorer implements an Downloader from the local directory
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frate_limit b/services/migrations/testdata/github/full_download/GET_%2Frate_limit
index f3a1c10f1d..74e43a0765 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frate_limit
+++ b/services/migrations/testdata/github/full_download/GET_%2Frate_limit
@@ -1,24 +1,23 @@
Cache-Control: no-cache
-X-Ratelimit-Used: 136
-X-Ratelimit-Resource: core
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Frame-Options: deny
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Github-Request-Id: E80A:118F3A:208966:1EA2B8:689B4023
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Limit: 5000
-Vary: Accept-Encoding, Accept, X-Requested-With
-Content-Type: application/json; charset=utf-8
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes:
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Remaining: 4864
-X-Ratelimit-Reset: 1755007969
+X-Ratelimit-Reset: 1730800941
Access-Control-Allow-Origin: *
-X-Content-Type-Options: nosniff
+Content-Type: application/json; charset=utf-8
+X-Oauth-Scopes:
X-Github-Media-Type: github.v3; format=json
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Ratelimit-Remaining: 4899
X-Xss-Protection: 0
Content-Security-Policy: default-src 'none'
+X-Accepted-Oauth-Scopes:
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Frame-Options: deny
+X-Content-Type-Options: nosniff
+Vary: Accept-Encoding, Accept, X-Requested-With
+X-Github-Request-Id: C7CC:3118FC:3F6234D:4038C5B:6729E6C0
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Used: 101
+X-Ratelimit-Resource: core
-{"resources":{"core":{"limit":5000,"used":136,"remaining":4864,"reset":1755007969},"search":{"limit":30,"used":0,"remaining":30,"reset":1755005023},"graphql":{"limit":5000,"used":0,"remaining":5000,"reset":1755008563},"integration_manifest":{"limit":5000,"used":0,"remaining":5000,"reset":1755008563},"source_import":{"limit":100,"used":0,"remaining":100,"reset":1755005023},"code_scanning_upload":{"limit":5000,"used":136,"remaining":4864,"reset":1755007969},"code_scanning_autofix":{"limit":10,"used":0,"remaining":10,"reset":1755005023},"actions_runner_registration":{"limit":10000,"used":0,"remaining":10000,"reset":1755008563},"scim":{"limit":15000,"used":0,"remaining":15000,"reset":1755008563},"dependency_snapshots":{"limit":100,"used":0,"remaining":100,"reset":1755005023},"dependency_sbom":{"limit":100,"used":0,"remaining":100,"reset":1755005023},"audit_log":{"limit":1750,"used":0,"remaining":1750,"reset":1755008563},"audit_log_streaming":{"limit":15,"used":0,"remaining":15,"reset":1755008563},"code_search":{"limit":10,"used":0,"remaining":10,"reset":1755005023}},"rate":{"limit":5000,"used":136,"remaining":4864,"reset":1755007969}}
\ No newline at end of file
+{"resources":{"core":{"limit":5000,"used":101,"remaining":4899,"reset":1730800941},"search":{"limit":30,"used":0,"remaining":30,"reset":1730799356},"graphql":{"limit":5000,"used":162,"remaining":4838,"reset":1730801064},"integration_manifest":{"limit":5000,"used":0,"remaining":5000,"reset":1730802896},"source_import":{"limit":100,"used":0,"remaining":100,"reset":1730799356},"code_scanning_upload":{"limit":1000,"used":0,"remaining":1000,"reset":1730802896},"actions_runner_registration":{"limit":10000,"used":0,"remaining":10000,"reset":1730802896},"scim":{"limit":15000,"used":0,"remaining":15000,"reset":1730802896},"dependency_snapshots":{"limit":100,"used":0,"remaining":100,"reset":1730799356},"audit_log":{"limit":1750,"used":0,"remaining":1750,"reset":1730802896},"audit_log_streaming":{"limit":15,"used":0,"remaining":15,"reset":1730802896},"code_search":{"limit":10,"used":0,"remaining":10,"reset":1730799356}},"rate":{"limit":5000,"used":101,"remaining":4899,"reset":1730800941}}
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo
deleted file mode 100644
index 57e87a4775..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo
+++ /dev/null
@@ -1,26 +0,0 @@
-X-Ratelimit-Remaining: 4880
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Content-Type: application/json; charset=utf-8
-Etag: W/"86b7478cb9d7f78696810f3292315b35ab47125e71dd2315dfd41626383fc081"
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Resource: core
-Access-Control-Allow-Origin: *
-X-Content-Type-Options: nosniff
-Content-Security-Policy: default-src 'none'
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Accepted-Oauth-Scopes: repo
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Used: 120
-X-Frame-Options: deny
-Last-Modified: Tue, 12 Aug 2025 11:21:45 GMT
-X-Oauth-Scopes: public_repo, repo:status
-X-Xss-Protection: 0
-X-Github-Request-Id: E80A:118F3A:20561B:1E7279:689B401B
-Cache-Control: private, max-age=60, s-maxage=60
-X-Github-Media-Type: github.v3; param=scarlet-witch-preview; format=json, github.mercy-preview; param=baptiste-preview.nebula-preview; format=json
-X-Ratelimit-Limit: 5000
-
-{"id":1033841924,"node_id":"R_kgDOPZ8tBA","name":"test_repo","full_name":"forgejo/test_repo","private":false,"owner":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/forgejo/test_repo","description":"Exclusively used for testing Github->Forgejo migration","fork":false,"url":"https://api.github.com/repos/forgejo/test_repo","forks_url":"https://api.github.com/repos/forgejo/test_repo/forks","keys_url":"https://api.github.com/repos/forgejo/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/forgejo/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/forgejo/test_repo/teams","hooks_url":"https://api.github.com/repos/forgejo/test_repo/hooks","issue_events_url":"https://api.github.com/repos/forgejo/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/forgejo/test_repo/events","assignees_url":"https://api.github.com/repos/forgejo/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/forgejo/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/forgejo/test_repo/tags","blobs_url":"https://api.github.com/repos/forgejo/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/forgejo/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/forgejo/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/forgejo/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/forgejo/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/forgejo/test_repo/languages","stargazers_url":"https://api.github.com/repos/forgejo/test_repo/stargazers","contributors_url":"https://api.github.com/repos/forgejo/test_repo/contributors","subscribers_url":"https://api.github.com/repos/forgejo/test_repo/subscribers","subscription_url":"https://api.github.com/repos/forgejo/test_repo/subscription","commits_url":"https://api.github.com/repos/forgejo/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/forgejo/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/forgejo/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/forgejo/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/forgejo/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/forgejo/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/forgejo/test_repo/merges","archive_url":"https://api.github.com/repos/forgejo/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/forgejo/test_repo/downloads","issues_url":"https://api.github.com/repos/forgejo/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/forgejo/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/forgejo/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/forgejo/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/forgejo/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/forgejo/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/forgejo/test_repo/deployments","created_at":"2025-08-07T12:34:21Z","updated_at":"2025-08-12T11:21:45Z","pushed_at":"2025-08-07T13:07:49Z","git_url":"git://github.com/forgejo/test_repo.git","ssh_url":"git@github.com:forgejo/test_repo.git","clone_url":"https://github.com/forgejo/test_repo.git","svn_url":"https://github.com/forgejo/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":6,"stargazers_count":1,"watchers_count":1,"language":null,"has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":{"key":"0bsd","name":"BSD Zero Clause License","spdx_id":"0BSD","url":"https://api.github.com/licenses/0bsd","node_id":"MDc6TGljZW5zZTM1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["forgejo"],"visibility":"public","forks":1,"open_issues":5,"watchers":1,"default_branch":"main","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE","custom_properties":{},"organization":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"security_and_analysis":{"secret_scanning":{"status":"disabled"},"secret_scanning_push_protection":{"status":"disabled"},"dependabot_security_updates":{"status":"disabled"},"secret_scanning_non_provider_patterns":{"status":"disabled"},"secret_scanning_validity_checks":{"status":"disabled"}},"network_count":1,"subscribers_count":0}
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created
deleted file mode 100644
index 5a45f886a9..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created
+++ /dev/null
@@ -1,25 +0,0 @@
-X-Content-Type-Options: nosniff
-X-Xss-Protection: 0
-Cache-Control: private, max-age=60, s-maxage=60
-X-Oauth-Scopes: public_repo, repo:status
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-X-Ratelimit-Reset: 1755007969
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Content-Security-Policy: default-src 'none'
-X-Github-Request-Id: E80A:118F3A:206AFD:1E8691:689B401E
-Content-Type: application/json; charset=utf-8
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Remaining: 4873
-X-Ratelimit-Used: 127
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Accepted-Oauth-Scopes:
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-Etag: W/"efada731cc49c5e9612cad9e891f65f645857f94ff3ea109dc80927064a5978c"
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Resource: core
-Access-Control-Allow-Origin: *
-X-Frame-Options: deny
-
-[{"url":"https://api.github.com/repos/forgejo/test_repo/issues/comments/3164123494","html_url":"https://github.com/forgejo/test_repo/issues/2#issuecomment-3164123494","issue_url":"https://api.github.com/repos/forgejo/test_repo/issues/2","id":3164123494,"node_id":"IC_kwDOPZ8tBM68mLFm","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2025-08-07T13:07:25Z","updated_at":"2025-08-07T13:07:25Z","author_association":"COLLABORATOR","body":"Mentioning #3 \nWith some **bold** *statement*","reactions":{"url":"https://api.github.com/repos/forgejo/test_repo/issues/comments/3164123494/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=2&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=2&per_page=2
deleted file mode 100644
index 7d978a2584..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=2&per_page=2
+++ /dev/null
@@ -1,27 +0,0 @@
-X-Ratelimit-Limit: 5000
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Xss-Protection: 0
-X-Github-Request-Id: F458:8333F:84A571:7C4748:689B3396
-Content-Type: application/json; charset=utf-8
-Content-Length: 2
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes: repo
-X-Ratelimit-Reset: 1755004338
-X-Content-Type-Options: nosniff
-Cache-Control: private, max-age=60, s-maxage=60
-X-Ratelimit-Used: 56
-X-Ratelimit-Resource: core
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Access-Control-Allow-Origin: *
-X-Frame-Options: deny
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-X-Ratelimit-Remaining: 4944
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Content-Security-Policy: default-src 'none'
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
-Link: ; rel="prev", ; rel="last", ; rel="first"
-X-Github-Api-Version-Selected: 2022-11-28
-
-[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%3Fdirection=asc&page=1&per_page=2&sort=created&state=all b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
deleted file mode 100644
index 6e74d44a20..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
+++ /dev/null
@@ -1,26 +0,0 @@
-Link: ; rel="next"
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Reset: 1755007969
-Access-Control-Allow-Origin: *
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Used: 124
-X-Frame-Options: deny
-X-Xss-Protection: 0
-Content-Security-Policy: default-src 'none'
-X-Github-Request-Id: E80A:118F3A:206102:1E7D47:689B401C
-Etag: W/"cc2f0f6e0b3c82ac10b12bf3ad5d74d62a575371f88fc689a7af78408e34eeb5"
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Remaining: 4876
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Content-Type: application/json; charset=utf-8
-Cache-Control: private, max-age=60, s-maxage=60
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes: repo
-X-Ratelimit-Resource: core
-X-Content-Type-Options: nosniff
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-
-[{"url":"https://api.github.com/repos/forgejo/test_repo/issues/1","repository_url":"https://api.github.com/repos/forgejo/test_repo","labels_url":"https://api.github.com/repos/forgejo/test_repo/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/forgejo/test_repo/issues/1/comments","events_url":"https://api.github.com/repos/forgejo/test_repo/issues/1/events","html_url":"https://github.com/forgejo/test_repo/issues/1","id":3300365512,"node_id":"I_kwDOPZ8tBM7Et5TI","number":1,"title":"First issue","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":1,"created_at":"2025-08-07T12:44:07Z","updated_at":"2025-08-07T12:44:47Z","closed_at":null,"author_association":"COLLABORATOR","type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"body":"This is an issue.","closed_by":null,"reactions":{"url":"https://api.github.com/repos/forgejo/test_repo/issues/1/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/forgejo/test_repo/issues/1/timeline","performed_via_github_app":null,"state_reason":null},{"url":"https://api.github.com/repos/forgejo/test_repo/issues/2","repository_url":"https://api.github.com/repos/forgejo/test_repo","labels_url":"https://api.github.com/repos/forgejo/test_repo/issues/2/labels{/name}","comments_url":"https://api.github.com/repos/forgejo/test_repo/issues/2/comments","events_url":"https://api.github.com/repos/forgejo/test_repo/issues/2/events","html_url":"https://github.com/forgejo/test_repo/issues/2","id":3300370333,"node_id":"I_kwDOPZ8tBM7Et6ed","number":2,"title":"Second Issue","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":9072229377,"node_id":"LA_kwDOPZ8tBM8AAAACHL88AQ","url":"https://api.github.com/repos/forgejo/test_repo/labels/duplicate","name":"duplicate","color":"cfd3d7","default":true,"description":"This issue or pull request already exists"},{"id":9072229408,"node_id":"LA_kwDOPZ8tBM8AAAACHL88IA","url":"https://api.github.com/repos/forgejo/test_repo/labels/good%20first%20issue","name":"good first issue","color":"7057ff","default":true,"description":"Good for newcomers"},{"id":9072229417,"node_id":"LA_kwDOPZ8tBM8AAAACHL88KQ","url":"https://api.github.com/repos/forgejo/test_repo/labels/help%20wanted","name":"help wanted","color":"008672","default":true,"description":"Extra attention is needed"}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":{"url":"https://api.github.com/repos/forgejo/test_repo/milestones/2","html_url":"https://github.com/forgejo/test_repo/milestone/2","labels_url":"https://api.github.com/repos/forgejo/test_repo/milestones/2/labels","id":13445587,"node_id":"MI_kwDOPZ8tBM4AzSnT","number":2,"title":"1.1.0","description":"We can do that","creator":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":0,"state":"open","created_at":"2025-08-07T12:50:58Z","updated_at":"2025-08-07T12:53:15Z","due_on":"2025-08-31T07:00:00Z","closed_at":null},"comments":1,"created_at":"2025-08-07T12:45:44Z","updated_at":"2025-08-07T13:07:25Z","closed_at":null,"author_association":"COLLABORATOR","type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"body":"Mentioning #1 ","closed_by":null,"reactions":{"url":"https://api.github.com/repos/forgejo/test_repo/issues/2/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/forgejo/test_repo/issues/2/timeline","performed_via_github_app":null,"state_reason":null}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Flabels%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Flabels%3Fpage=1&per_page=100
deleted file mode 100644
index ef1aebf7a8..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Flabels%3Fpage=1&per_page=100
+++ /dev/null
@@ -1,25 +0,0 @@
-Cache-Control: private, max-age=60, s-maxage=60
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Accepted-Oauth-Scopes: repo
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Reset: 1755007969
-X-Xss-Protection: 0
-X-Github-Request-Id: E80A:118F3A:205BDE:1E77C9:689B401C
-Content-Type: application/json; charset=utf-8
-Etag: W/"fc30186df2be93c9dbcbb326f3d0249e5d618f924528d71af1d11941dc37f8b0"
-X-Oauth-Scopes: public_repo, repo:status
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Github-Media-Type: github.v3; format=json
-X-Ratelimit-Limit: 5000
-Access-Control-Allow-Origin: *
-X-Frame-Options: deny
-X-Content-Type-Options: nosniff
-X-Ratelimit-Remaining: 4878
-X-Ratelimit-Used: 122
-X-Ratelimit-Resource: core
-Content-Security-Policy: default-src 'none'
-
-[{"id":9072229344,"node_id":"LA_kwDOPZ8tBM8AAAACHL874A","url":"https://api.github.com/repos/forgejo/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"},{"id":9072229361,"node_id":"LA_kwDOPZ8tBM8AAAACHL878Q","url":"https://api.github.com/repos/forgejo/test_repo/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"},{"id":9072229377,"node_id":"LA_kwDOPZ8tBM8AAAACHL88AQ","url":"https://api.github.com/repos/forgejo/test_repo/labels/duplicate","name":"duplicate","color":"cfd3d7","default":true,"description":"This issue or pull request already exists"},{"id":9072229390,"node_id":"LA_kwDOPZ8tBM8AAAACHL88Dg","url":"https://api.github.com/repos/forgejo/test_repo/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"},{"id":9072229408,"node_id":"LA_kwDOPZ8tBM8AAAACHL88IA","url":"https://api.github.com/repos/forgejo/test_repo/labels/good%20first%20issue","name":"good first issue","color":"7057ff","default":true,"description":"Good for newcomers"},{"id":9072229417,"node_id":"LA_kwDOPZ8tBM8AAAACHL88KQ","url":"https://api.github.com/repos/forgejo/test_repo/labels/help%20wanted","name":"help wanted","color":"008672","default":true,"description":"Extra attention is needed"},{"id":9072229426,"node_id":"LA_kwDOPZ8tBM8AAAACHL88Mg","url":"https://api.github.com/repos/forgejo/test_repo/labels/invalid","name":"invalid","color":"e4e669","default":true,"description":"This doesn't seem right"},{"id":9072229435,"node_id":"LA_kwDOPZ8tBM8AAAACHL88Ow","url":"https://api.github.com/repos/forgejo/test_repo/labels/question","name":"question","color":"d876e3","default":true,"description":"Further information is requested"},{"id":9072229442,"node_id":"LA_kwDOPZ8tBM8AAAACHL88Qg","url":"https://api.github.com/repos/forgejo/test_repo/labels/wontfix","name":"wontfix","color":"ffffff","default":true,"description":"This will not be worked on"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all
deleted file mode 100644
index 7dba1e4aac..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all
+++ /dev/null
@@ -1,25 +0,0 @@
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Github-Request-Id: E80A:118F3A:205928:1E754F:689B401B
-Etag: W/"39c17013c8c7b0f5abf91f15e52496c38730029baf6f37b5c3f3e40eb9886aab"
-X-Ratelimit-Remaining: 4879
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Xss-Protection: 0
-Content-Type: application/json; charset=utf-8
-Cache-Control: private, max-age=60, s-maxage=60
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Used: 121
-X-Ratelimit-Resource: core
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; format=json
-X-Github-Api-Version-Selected: 2022-11-28
-X-Frame-Options: deny
-X-Content-Type-Options: nosniff
-Content-Security-Policy: default-src 'none'
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes: repo
-X-Ratelimit-Limit: 5000
-Access-Control-Allow-Origin: *
-
-[{"url":"https://api.github.com/repos/forgejo/test_repo/milestones/1","html_url":"https://github.com/forgejo/test_repo/milestone/1","labels_url":"https://api.github.com/repos/forgejo/test_repo/milestones/1/labels","id":13445581,"node_id":"MI_kwDOPZ8tBM4AzSnN","number":1,"title":"1.0.0","description":"Version 1","creator":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":2,"state":"open","created_at":"2025-08-07T12:48:56Z","updated_at":"2025-08-12T12:34:20Z","due_on":null,"closed_at":null},{"url":"https://api.github.com/repos/forgejo/test_repo/milestones/3","html_url":"https://github.com/forgejo/test_repo/milestone/3","labels_url":"https://api.github.com/repos/forgejo/test_repo/milestones/3/labels","id":13445604,"node_id":"MI_kwDOPZ8tBM4AzSnk","number":3,"title":"0.9.0","description":"A milestone","creator":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":0,"state":"closed","created_at":"2025-08-07T12:54:20Z","updated_at":"2025-08-12T11:29:52Z","due_on":"2025-08-01T07:00:00Z","closed_at":"2025-08-07T12:54:38Z"},{"url":"https://api.github.com/repos/forgejo/test_repo/milestones/2","html_url":"https://github.com/forgejo/test_repo/milestone/2","labels_url":"https://api.github.com/repos/forgejo/test_repo/milestones/2/labels","id":13445587,"node_id":"MI_kwDOPZ8tBM4AzSnT","number":2,"title":"1.1.0","description":"We can do that","creator":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":0,"state":"open","created_at":"2025-08-07T12:50:58Z","updated_at":"2025-08-07T12:53:15Z","due_on":"2025-08-31T07:00:00Z","closed_at":null}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3096999684%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3096999684%2Fcomments%3Fper_page=100
deleted file mode 100644
index 51f70d2a81..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3096999684%2Fcomments%3Fper_page=100
+++ /dev/null
@@ -1,25 +0,0 @@
-Content-Type: application/json; charset=utf-8
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-Etag: W/"9f6ff2d639a65dc556bded7ae25926c53ac6673582b68d1ebbbe3ecb1c375e3b"
-X-Ratelimit-Limit: 5000
-Access-Control-Allow-Origin: *
-X-Content-Type-Options: nosniff
-X-Xss-Protection: 0
-Content-Security-Policy: default-src 'none'
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes:
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Remaining: 4867
-X-Ratelimit-Resource: core
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Github-Request-Id: E80A:118F3A:207FBB:1E9958:689B4021
-Cache-Control: private, max-age=60, s-maxage=60
-X-Github-Media-Type: github.v3; format=json
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Used: 133
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Frame-Options: deny
-
-[{"id":2260216729,"node_id":"PRRC_kwDOPZ8tBM6GuCuZ","url":"https://api.github.com/repos/forgejo/test_repo/pulls/comments/2260216729","pull_request_review_id":3096999684,"diff_hunk":"@@ -1,3 +1,5 @@\n # Forgejo Test Repo\n \n This repo is used to test migrations\n+\n+Add some feature description.","path":"readme.md","position":5,"original_position":5,"commit_id":"c608ab3997349219e1510cdb5ddd1e5e82897dfa","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"May want to write more","created_at":"2025-08-07T12:47:50Z","updated_at":"2025-08-07T12:47:55Z","html_url":"https://github.com/forgejo/test_repo/pull/3#discussion_r2260216729","pull_request_url":"https://api.github.com/repos/forgejo/test_repo/pulls/3","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/comments/2260216729"},"html":{"href":"https://github.com/forgejo/test_repo/pull/3#discussion_r2260216729"},"pull_request":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/3"}},"original_commit_id":"c608ab3997349219e1510cdb5ddd1e5e82897dfa","reactions":{"url":"https://api.github.com/repos/forgejo/test_repo/pulls/comments/2260216729/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0}}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3097007243%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3097007243%2Fcomments%3Fper_page=100
deleted file mode 100644
index cd5fbdaf9d..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3097007243%2Fcomments%3Fper_page=100
+++ /dev/null
@@ -1,25 +0,0 @@
-Etag: W/"17a2560299e1dbb8e8b83de09a8a409cd7b735baeebea469a041311a71a948d0"
-X-Accepted-Oauth-Scopes:
-X-Ratelimit-Remaining: 4865
-X-Ratelimit-Resource: core
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Content-Security-Policy: default-src 'none'
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Used: 135
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Github-Request-Id: E80A:118F3A:20854A:1E9EB1:689B4022
-Content-Type: application/json; charset=utf-8
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Github-Media-Type: github.v3; format=json
-X-Ratelimit-Limit: 5000
-X-Frame-Options: deny
-X-Content-Type-Options: nosniff
-X-Xss-Protection: 0
-X-Oauth-Scopes: public_repo, repo:status
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Reset: 1755007969
-Access-Control-Allow-Origin: *
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Cache-Control: private, max-age=60, s-maxage=60
-
-[{"id":2260221159,"node_id":"PRRC_kwDOPZ8tBM6GuDzn","url":"https://api.github.com/repos/forgejo/test_repo/pulls/comments/2260221159","pull_request_review_id":3097007243,"diff_hunk":"@@ -1,3 +1,5 @@\n # Forgejo Test Repo\n \n This repo is used to test migrations","path":"readme.md","position":3,"original_position":3,"commit_id":"c608ab3997349219e1510cdb5ddd1e5e82897dfa","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Comment","created_at":"2025-08-07T12:49:36Z","updated_at":"2025-08-07T12:49:36Z","html_url":"https://github.com/forgejo/test_repo/pull/3#discussion_r2260221159","pull_request_url":"https://api.github.com/repos/forgejo/test_repo/pulls/3","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/comments/2260221159"},"html":{"href":"https://github.com/forgejo/test_repo/pull/3#discussion_r2260221159"},"pull_request":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/3"}},"original_commit_id":"c608ab3997349219e1510cdb5ddd1e5e82897dfa","reactions":{"url":"https://api.github.com/repos/forgejo/test_repo/pulls/comments/2260221159/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0}}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100
deleted file mode 100644
index 82970c7a67..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100
+++ /dev/null
@@ -1,25 +0,0 @@
-Content-Type: application/json; charset=utf-8
-X-Accepted-Oauth-Scopes:
-X-Github-Media-Type: github.v3; format=json
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Content-Type-Options: nosniff
-Content-Security-Policy: default-src 'none'
-Cache-Control: private, max-age=60, s-maxage=60
-Etag: W/"d3281ac301ca94c8125009c335025cae84e5af242cb315bb975afad875b825e2"
-X-Oauth-Scopes: public_repo, repo:status
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Remaining: 4868
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Github-Request-Id: E80A:118F3A:207BE7:1E9600:689B4020
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Resource: core
-Access-Control-Allow-Origin: *
-X-Frame-Options: deny
-X-Xss-Protection: 0
-X-Ratelimit-Used: 132
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-
-[{"id":3096999684,"node_id":"PRR_kwDOPZ8tBM64mHcE","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?u=c4857996d3079f843b3eb2a7dcb347698d817034&v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"COMMENTED","html_url":"https://github.com/forgejo/test_repo/pull/3#pullrequestreview-3096999684","pull_request_url":"https://api.github.com/repos/forgejo/test_repo/pulls/3","author_association":"COLLABORATOR","_links":{"html":{"href":"https://github.com/forgejo/test_repo/pull/3#pullrequestreview-3096999684"},"pull_request":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/3"}},"submitted_at":"2025-08-07T12:47:55Z","commit_id":"c608ab3997349219e1510cdb5ddd1e5e82897dfa"},{"id":3097007243,"node_id":"PRR_kwDOPZ8tBM64mJSL","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?u=c4857996d3079f843b3eb2a7dcb347698d817034&v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"COMMENTED","html_url":"https://github.com/forgejo/test_repo/pull/3#pullrequestreview-3097007243","pull_request_url":"https://api.github.com/repos/forgejo/test_repo/pulls/3","author_association":"COLLABORATOR","_links":{"html":{"href":"https://github.com/forgejo/test_repo/pull/3#pullrequestreview-3097007243"},"pull_request":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/3"}},"submitted_at":"2025-08-07T12:49:36Z","commit_id":"c608ab3997349219e1510cdb5ddd1e5e82897dfa"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
deleted file mode 100644
index eea4344781..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
+++ /dev/null
@@ -1,23 +0,0 @@
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Remaining: 4856
-X-Ratelimit-Used: 144
-X-Accepted-Oauth-Scopes: repo
-X-Github-Media-Type: github.v3; format=json
-X-Github-Api-Version-Selected: 2022-11-28
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Xss-Protection: 0
-Content-Security-Policy: default-src 'none'
-Vary: Accept-Encoding, Accept, X-Requested-With
-X-Github-Request-Id: EDE1:32C4F6:A76DB4:9D7693:689B37A6
-X-Ratelimit-Reset: 1755004338
-X-Ratelimit-Resource: core
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Frame-Options: deny
-Content-Type: application/json; charset=utf-8
-X-Oauth-Scopes: public_repo, repo:status
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-Access-Control-Allow-Origin: *
-X-Content-Type-Options: nosniff
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-
-{"message":"Not Found","documentation_url":"https://docs.github.com/rest/pulls/reviews#list-reviews-for-a-pull-request","status":"404"}
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%3Fdirection=asc&page=1&per_page=2&sort=created&state=all b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
deleted file mode 100644
index 2835f22e9b..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
+++ /dev/null
@@ -1,25 +0,0 @@
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Frame-Options: deny
-Cache-Control: private, max-age=60, s-maxage=60
-X-Accepted-Oauth-Scopes:
-X-Ratelimit-Remaining: 4871
-X-Content-Type-Options: nosniff
-Content-Security-Policy: default-src 'none'
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; format=json
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Reset: 1755007969
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Access-Control-Allow-Origin: *
-X-Xss-Protection: 0
-X-Github-Request-Id: E80A:118F3A:207219:1E8D78:689B401F
-Content-Type: application/json; charset=utf-8
-X-Oauth-Scopes: public_repo, repo:status
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Used: 129
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-Etag: W/"9669b72db2e9016fd408b8c554dc01719c614d29bb965b082a81ee0489fd0914"
-X-Ratelimit-Resource: core
-
-[{"url":"https://api.github.com/repos/forgejo/test_repo/pulls/3","id":2727677736,"node_id":"PR_kwDOPZ8tBM6ilQ8o","html_url":"https://github.com/forgejo/test_repo/pull/3","diff_url":"https://github.com/forgejo/test_repo/pull/3.diff","patch_url":"https://github.com/forgejo/test_repo/pull/3.patch","issue_url":"https://api.github.com/repos/forgejo/test_repo/issues/3","number":3,"state":"open","locked":false,"title":"Update readme.md","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Added a feature description","created_at":"2025-08-07T12:47:06Z","updated_at":"2025-08-12T13:16:49Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":9072229390,"node_id":"LA_kwDOPZ8tBM8AAAACHL88Dg","url":"https://api.github.com/repos/forgejo/test_repo/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"}],"milestone":{"url":"https://api.github.com/repos/forgejo/test_repo/milestones/1","html_url":"https://github.com/forgejo/test_repo/milestone/1","labels_url":"https://api.github.com/repos/forgejo/test_repo/milestones/1/labels","id":13445581,"node_id":"MI_kwDOPZ8tBM4AzSnN","number":1,"title":"1.0.0","description":"Version 1","creator":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":2,"state":"open","created_at":"2025-08-07T12:48:56Z","updated_at":"2025-08-12T12:34:20Z","due_on":null,"closed_at":null},"draft":false,"commits_url":"https://api.github.com/repos/forgejo/test_repo/pulls/3/commits","review_comments_url":"https://api.github.com/repos/forgejo/test_repo/pulls/3/comments","review_comment_url":"https://api.github.com/repos/forgejo/test_repo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/forgejo/test_repo/issues/3/comments","statuses_url":"https://api.github.com/repos/forgejo/test_repo/statuses/c608ab3997349219e1510cdb5ddd1e5e82897dfa","head":{"label":"forgejo:some-feature","ref":"some-feature","sha":"c608ab3997349219e1510cdb5ddd1e5e82897dfa","user":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1033841924,"node_id":"R_kgDOPZ8tBA","name":"test_repo","full_name":"forgejo/test_repo","private":false,"owner":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/forgejo/test_repo","description":"Exclusively used for testing Github->Forgejo migration","fork":false,"url":"https://api.github.com/repos/forgejo/test_repo","forks_url":"https://api.github.com/repos/forgejo/test_repo/forks","keys_url":"https://api.github.com/repos/forgejo/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/forgejo/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/forgejo/test_repo/teams","hooks_url":"https://api.github.com/repos/forgejo/test_repo/hooks","issue_events_url":"https://api.github.com/repos/forgejo/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/forgejo/test_repo/events","assignees_url":"https://api.github.com/repos/forgejo/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/forgejo/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/forgejo/test_repo/tags","blobs_url":"https://api.github.com/repos/forgejo/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/forgejo/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/forgejo/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/forgejo/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/forgejo/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/forgejo/test_repo/languages","stargazers_url":"https://api.github.com/repos/forgejo/test_repo/stargazers","contributors_url":"https://api.github.com/repos/forgejo/test_repo/contributors","subscribers_url":"https://api.github.com/repos/forgejo/test_repo/subscribers","subscription_url":"https://api.github.com/repos/forgejo/test_repo/subscription","commits_url":"https://api.github.com/repos/forgejo/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/forgejo/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/forgejo/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/forgejo/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/forgejo/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/forgejo/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/forgejo/test_repo/merges","archive_url":"https://api.github.com/repos/forgejo/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/forgejo/test_repo/downloads","issues_url":"https://api.github.com/repos/forgejo/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/forgejo/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/forgejo/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/forgejo/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/forgejo/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/forgejo/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/forgejo/test_repo/deployments","created_at":"2025-08-07T12:34:21Z","updated_at":"2025-08-12T11:21:45Z","pushed_at":"2025-08-07T13:07:49Z","git_url":"git://github.com/forgejo/test_repo.git","ssh_url":"git@github.com:forgejo/test_repo.git","clone_url":"https://github.com/forgejo/test_repo.git","svn_url":"https://github.com/forgejo/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":6,"stargazers_count":1,"watchers_count":1,"language":null,"has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":{"key":"0bsd","name":"BSD Zero Clause License","spdx_id":"0BSD","url":"https://api.github.com/licenses/0bsd","node_id":"MDc6TGljZW5zZTM1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["forgejo"],"visibility":"public","forks":1,"open_issues":5,"watchers":1,"default_branch":"main"}},"base":{"label":"forgejo:main","ref":"main","sha":"442d28a55b842472c95bead51a4c61f209ac1636","user":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1033841924,"node_id":"R_kgDOPZ8tBA","name":"test_repo","full_name":"forgejo/test_repo","private":false,"owner":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/forgejo/test_repo","description":"Exclusively used for testing Github->Forgejo migration","fork":false,"url":"https://api.github.com/repos/forgejo/test_repo","forks_url":"https://api.github.com/repos/forgejo/test_repo/forks","keys_url":"https://api.github.com/repos/forgejo/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/forgejo/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/forgejo/test_repo/teams","hooks_url":"https://api.github.com/repos/forgejo/test_repo/hooks","issue_events_url":"https://api.github.com/repos/forgejo/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/forgejo/test_repo/events","assignees_url":"https://api.github.com/repos/forgejo/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/forgejo/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/forgejo/test_repo/tags","blobs_url":"https://api.github.com/repos/forgejo/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/forgejo/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/forgejo/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/forgejo/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/forgejo/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/forgejo/test_repo/languages","stargazers_url":"https://api.github.com/repos/forgejo/test_repo/stargazers","contributors_url":"https://api.github.com/repos/forgejo/test_repo/contributors","subscribers_url":"https://api.github.com/repos/forgejo/test_repo/subscribers","subscription_url":"https://api.github.com/repos/forgejo/test_repo/subscription","commits_url":"https://api.github.com/repos/forgejo/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/forgejo/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/forgejo/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/forgejo/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/forgejo/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/forgejo/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/forgejo/test_repo/merges","archive_url":"https://api.github.com/repos/forgejo/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/forgejo/test_repo/downloads","issues_url":"https://api.github.com/repos/forgejo/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/forgejo/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/forgejo/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/forgejo/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/forgejo/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/forgejo/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/forgejo/test_repo/deployments","created_at":"2025-08-07T12:34:21Z","updated_at":"2025-08-12T11:21:45Z","pushed_at":"2025-08-07T13:07:49Z","git_url":"git://github.com/forgejo/test_repo.git","ssh_url":"git@github.com:forgejo/test_repo.git","clone_url":"https://github.com/forgejo/test_repo.git","svn_url":"https://github.com/forgejo/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":6,"stargazers_count":1,"watchers_count":1,"language":null,"has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":{"key":"0bsd","name":"BSD Zero Clause License","spdx_id":"0BSD","url":"https://api.github.com/licenses/0bsd","node_id":"MDc6TGljZW5zZTM1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["forgejo"],"visibility":"public","forks":1,"open_issues":5,"watchers":1,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/3"},"html":{"href":"https://github.com/forgejo/test_repo/pull/3"},"issue":{"href":"https://api.github.com/repos/forgejo/test_repo/issues/3"},"comments":{"href":"https://api.github.com/repos/forgejo/test_repo/issues/3/comments"},"review_comments":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/3/comments"},"review_comment":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/3/commits"},"statuses":{"href":"https://api.github.com/repos/forgejo/test_repo/statuses/c608ab3997349219e1510cdb5ddd1e5e82897dfa"}},"author_association":"COLLABORATOR","auto_merge":null,"active_lock_reason":null},{"url":"https://api.github.com/repos/forgejo/test_repo/pulls/7","id":2727724283,"node_id":"PR_kwDOPZ8tBM6ilcT7","html_url":"https://github.com/forgejo/test_repo/pull/7","diff_url":"https://github.com/forgejo/test_repo/pull/7.diff","patch_url":"https://github.com/forgejo/test_repo/pull/7.patch","issue_url":"https://api.github.com/repos/forgejo/test_repo/issues/7","number":7,"state":"closed","locked":false,"title":"Update readme.md","user":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Adding some text to the readme","created_at":"2025-08-07T13:01:36Z","updated_at":"2025-08-12T12:47:35Z","closed_at":"2025-08-07T13:02:19Z","merged_at":"2025-08-07T13:02:19Z","merge_commit_sha":"ca43b48ca2c461f9a5cb66500a154b23d07c9f90","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":9072229344,"node_id":"LA_kwDOPZ8tBM8AAAACHL874A","url":"https://api.github.com/repos/forgejo/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"}],"milestone":{"url":"https://api.github.com/repos/forgejo/test_repo/milestones/1","html_url":"https://github.com/forgejo/test_repo/milestone/1","labels_url":"https://api.github.com/repos/forgejo/test_repo/milestones/1/labels","id":13445581,"node_id":"MI_kwDOPZ8tBM4AzSnN","number":1,"title":"1.0.0","description":"Version 1","creator":{"login":"PatDyn","id":37243484,"node_id":"MDQ6VXNlcjM3MjQzNDg0","avatar_url":"https://avatars.githubusercontent.com/u/37243484?v=4","gravatar_id":"","url":"https://api.github.com/users/PatDyn","html_url":"https://github.com/PatDyn","followers_url":"https://api.github.com/users/PatDyn/followers","following_url":"https://api.github.com/users/PatDyn/following{/other_user}","gists_url":"https://api.github.com/users/PatDyn/gists{/gist_id}","starred_url":"https://api.github.com/users/PatDyn/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/PatDyn/subscriptions","organizations_url":"https://api.github.com/users/PatDyn/orgs","repos_url":"https://api.github.com/users/PatDyn/repos","events_url":"https://api.github.com/users/PatDyn/events{/privacy}","received_events_url":"https://api.github.com/users/PatDyn/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":2,"state":"open","created_at":"2025-08-07T12:48:56Z","updated_at":"2025-08-12T12:34:20Z","due_on":null,"closed_at":null},"draft":false,"commits_url":"https://api.github.com/repos/forgejo/test_repo/pulls/7/commits","review_comments_url":"https://api.github.com/repos/forgejo/test_repo/pulls/7/comments","review_comment_url":"https://api.github.com/repos/forgejo/test_repo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/forgejo/test_repo/issues/7/comments","statuses_url":"https://api.github.com/repos/forgejo/test_repo/statuses/5638cb8f3278e467fc1eefcac14d3c0d5d91601f","head":{"label":"forgejo:another-feature","ref":"another-feature","sha":"5638cb8f3278e467fc1eefcac14d3c0d5d91601f","user":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1033841924,"node_id":"R_kgDOPZ8tBA","name":"test_repo","full_name":"forgejo/test_repo","private":false,"owner":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/forgejo/test_repo","description":"Exclusively used for testing Github->Forgejo migration","fork":false,"url":"https://api.github.com/repos/forgejo/test_repo","forks_url":"https://api.github.com/repos/forgejo/test_repo/forks","keys_url":"https://api.github.com/repos/forgejo/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/forgejo/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/forgejo/test_repo/teams","hooks_url":"https://api.github.com/repos/forgejo/test_repo/hooks","issue_events_url":"https://api.github.com/repos/forgejo/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/forgejo/test_repo/events","assignees_url":"https://api.github.com/repos/forgejo/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/forgejo/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/forgejo/test_repo/tags","blobs_url":"https://api.github.com/repos/forgejo/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/forgejo/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/forgejo/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/forgejo/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/forgejo/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/forgejo/test_repo/languages","stargazers_url":"https://api.github.com/repos/forgejo/test_repo/stargazers","contributors_url":"https://api.github.com/repos/forgejo/test_repo/contributors","subscribers_url":"https://api.github.com/repos/forgejo/test_repo/subscribers","subscription_url":"https://api.github.com/repos/forgejo/test_repo/subscription","commits_url":"https://api.github.com/repos/forgejo/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/forgejo/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/forgejo/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/forgejo/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/forgejo/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/forgejo/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/forgejo/test_repo/merges","archive_url":"https://api.github.com/repos/forgejo/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/forgejo/test_repo/downloads","issues_url":"https://api.github.com/repos/forgejo/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/forgejo/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/forgejo/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/forgejo/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/forgejo/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/forgejo/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/forgejo/test_repo/deployments","created_at":"2025-08-07T12:34:21Z","updated_at":"2025-08-12T11:21:45Z","pushed_at":"2025-08-07T13:07:49Z","git_url":"git://github.com/forgejo/test_repo.git","ssh_url":"git@github.com:forgejo/test_repo.git","clone_url":"https://github.com/forgejo/test_repo.git","svn_url":"https://github.com/forgejo/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":6,"stargazers_count":1,"watchers_count":1,"language":null,"has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":{"key":"0bsd","name":"BSD Zero Clause License","spdx_id":"0BSD","url":"https://api.github.com/licenses/0bsd","node_id":"MDc6TGljZW5zZTM1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["forgejo"],"visibility":"public","forks":1,"open_issues":5,"watchers":1,"default_branch":"main"}},"base":{"label":"forgejo:main","ref":"main","sha":"6dd0c6801ddbb7333787e73e99581279492ff449","user":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":1033841924,"node_id":"R_kgDOPZ8tBA","name":"test_repo","full_name":"forgejo/test_repo","private":false,"owner":{"login":"forgejo","id":118922216,"node_id":"O_kgDOBxab6A","avatar_url":"https://avatars.githubusercontent.com/u/118922216?v=4","gravatar_id":"","url":"https://api.github.com/users/forgejo","html_url":"https://github.com/forgejo","followers_url":"https://api.github.com/users/forgejo/followers","following_url":"https://api.github.com/users/forgejo/following{/other_user}","gists_url":"https://api.github.com/users/forgejo/gists{/gist_id}","starred_url":"https://api.github.com/users/forgejo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/forgejo/subscriptions","organizations_url":"https://api.github.com/users/forgejo/orgs","repos_url":"https://api.github.com/users/forgejo/repos","events_url":"https://api.github.com/users/forgejo/events{/privacy}","received_events_url":"https://api.github.com/users/forgejo/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/forgejo/test_repo","description":"Exclusively used for testing Github->Forgejo migration","fork":false,"url":"https://api.github.com/repos/forgejo/test_repo","forks_url":"https://api.github.com/repos/forgejo/test_repo/forks","keys_url":"https://api.github.com/repos/forgejo/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/forgejo/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/forgejo/test_repo/teams","hooks_url":"https://api.github.com/repos/forgejo/test_repo/hooks","issue_events_url":"https://api.github.com/repos/forgejo/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/forgejo/test_repo/events","assignees_url":"https://api.github.com/repos/forgejo/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/forgejo/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/forgejo/test_repo/tags","blobs_url":"https://api.github.com/repos/forgejo/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/forgejo/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/forgejo/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/forgejo/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/forgejo/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/forgejo/test_repo/languages","stargazers_url":"https://api.github.com/repos/forgejo/test_repo/stargazers","contributors_url":"https://api.github.com/repos/forgejo/test_repo/contributors","subscribers_url":"https://api.github.com/repos/forgejo/test_repo/subscribers","subscription_url":"https://api.github.com/repos/forgejo/test_repo/subscription","commits_url":"https://api.github.com/repos/forgejo/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/forgejo/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/forgejo/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/forgejo/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/forgejo/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/forgejo/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/forgejo/test_repo/merges","archive_url":"https://api.github.com/repos/forgejo/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/forgejo/test_repo/downloads","issues_url":"https://api.github.com/repos/forgejo/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/forgejo/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/forgejo/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/forgejo/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/forgejo/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/forgejo/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/forgejo/test_repo/deployments","created_at":"2025-08-07T12:34:21Z","updated_at":"2025-08-12T11:21:45Z","pushed_at":"2025-08-07T13:07:49Z","git_url":"git://github.com/forgejo/test_repo.git","ssh_url":"git@github.com:forgejo/test_repo.git","clone_url":"https://github.com/forgejo/test_repo.git","svn_url":"https://github.com/forgejo/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":6,"stargazers_count":1,"watchers_count":1,"language":null,"has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":5,"license":{"key":"0bsd","name":"BSD Zero Clause License","spdx_id":"0BSD","url":"https://api.github.com/licenses/0bsd","node_id":"MDc6TGljZW5zZTM1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["forgejo"],"visibility":"public","forks":1,"open_issues":5,"watchers":1,"default_branch":"main"}},"_links":{"self":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/7"},"html":{"href":"https://github.com/forgejo/test_repo/pull/7"},"issue":{"href":"https://api.github.com/repos/forgejo/test_repo/issues/7"},"comments":{"href":"https://api.github.com/repos/forgejo/test_repo/issues/7/comments"},"review_comments":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/7/comments"},"review_comment":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/forgejo/test_repo/pulls/7/commits"},"statuses":{"href":"https://api.github.com/repos/forgejo/test_repo/statuses/5638cb8f3278e467fc1eefcac14d3c0d5d91601f"}},"author_association":"COLLABORATOR","auto_merge":null,"active_lock_reason":null}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Freleases%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Freleases%3Fpage=1&per_page=100
deleted file mode 100644
index 5fc5f2dc94..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Freleases%3Fpage=1&per_page=100
+++ /dev/null
@@ -1,25 +0,0 @@
-X-Ratelimit-Remaining: 4877
-X-Ratelimit-Used: 123
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Content-Security-Policy: default-src 'none'
-Content-Type: application/json; charset=utf-8
-Cache-Control: private, max-age=60, s-maxage=60
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Resource: core
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Xss-Protection: 0
-Etag: W/"da1b952735d448e2474ba8dc4ba5b9c2a1989fb8bf0002dff577ae452601af43"
-X-Github-Media-Type: github.v3; format=json
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Reset: 1755007969
-Access-Control-Allow-Origin: *
-X-Frame-Options: deny
-X-Content-Type-Options: nosniff
-X-Github-Request-Id: E80A:118F3A:205E1D:1E7A62:689B401C
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes: repo
-X-Ratelimit-Limit: 5000
-
-[{"url":"https://api.github.com/repos/forgejo/test_repo/releases/238309022","assets_url":"https://api.github.com/repos/forgejo/test_repo/releases/238309022/assets","upload_url":"https://uploads.github.com/repos/forgejo/test_repo/releases/238309022/assets{?name,label}","html_url":"https://github.com/forgejo/test_repo/releases/tag/v1.0","id":238309022,"author":{"login":"Gusted","id":25481501,"node_id":"MDQ6VXNlcjI1NDgxNTAx","avatar_url":"https://avatars.githubusercontent.com/u/25481501?v=4","gravatar_id":"","url":"https://api.github.com/users/Gusted","html_url":"https://github.com/Gusted","followers_url":"https://api.github.com/users/Gusted/followers","following_url":"https://api.github.com/users/Gusted/following{/other_user}","gists_url":"https://api.github.com/users/Gusted/gists{/gist_id}","starred_url":"https://api.github.com/users/Gusted/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Gusted/subscriptions","organizations_url":"https://api.github.com/users/Gusted/orgs","repos_url":"https://api.github.com/users/Gusted/repos","events_url":"https://api.github.com/users/Gusted/events{/privacy}","received_events_url":"https://api.github.com/users/Gusted/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"RE_kwDOPZ8tBM4ONE6e","tag_name":"v1.0","target_commitish":"main","name":"First Release","draft":false,"immutable":false,"prerelease":false,"created_at":"2025-08-07T13:02:19Z","updated_at":"2025-08-12T11:24:30Z","published_at":"2025-08-07T13:07:49Z","assets":[{"url":"https://api.github.com/repos/forgejo/test_repo/releases/assets/280443629","id":280443629,"node_id":"RA_kwDOPZ8tBM4Qtzrt","name":"wireguard.pdf","label":null,"uploader":{"login":"Gusted","id":25481501,"node_id":"MDQ6VXNlcjI1NDgxNTAx","avatar_url":"https://avatars.githubusercontent.com/u/25481501?v=4","gravatar_id":"","url":"https://api.github.com/users/Gusted","html_url":"https://github.com/Gusted","followers_url":"https://api.github.com/users/Gusted/followers","following_url":"https://api.github.com/users/Gusted/following{/other_user}","gists_url":"https://api.github.com/users/Gusted/gists{/gist_id}","starred_url":"https://api.github.com/users/Gusted/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Gusted/subscriptions","organizations_url":"https://api.github.com/users/Gusted/orgs","repos_url":"https://api.github.com/users/Gusted/repos","events_url":"https://api.github.com/users/Gusted/events{/privacy}","received_events_url":"https://api.github.com/users/Gusted/received_events","type":"User","user_view_type":"public","site_admin":false},"content_type":"application/pdf","state":"uploaded","size":550175,"digest":"sha256:b4cf398c21d054e8774af568395b0f7cd0e2b01322809c5dbd66c4135021ca57","download_count":0,"created_at":"2025-08-07T23:39:27Z","updated_at":"2025-08-07T23:39:29Z","browser_download_url":"https://github.com/forgejo/test_repo/releases/download/v1.0/wireguard.pdf"}],"tarball_url":"https://api.github.com/repos/forgejo/test_repo/tarball/v1.0","zipball_url":"https://api.github.com/repos/forgejo/test_repo/zipball/v1.0","body":"Hi, this is the first release! The asset contains the wireguard whitepaper, amazing read for such a simple protocol."}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo
new file mode 100644
index 0000000000..78fde4d424
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo
@@ -0,0 +1,25 @@
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Resource: core
+Access-Control-Allow-Origin: *
+X-Github-Request-Id: C7CC:3118FC:3F5EFD7:403585D:6729E6B3
+Cache-Control: private, max-age=60, s-maxage=60
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Media-Type: github.v3; param=scarlet-witch-preview; format=json, github.mercy-preview; param=baptiste-preview.nebula-preview; format=json
+Etag: W/"bb1c9e0186e52dbd9f2c34aaf0827517384a15fd0cee7b81ad13784901db15c0"
+X-Oauth-Scopes:
+X-Accepted-Oauth-Scopes: repo
+X-Ratelimit-Limit: 5000
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Content-Type-Options: nosniff
+Content-Type: application/json; charset=utf-8
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+Last-Modified: Thu, 02 Mar 2023 14:02:26 GMT
+X-Ratelimit-Remaining: 4928
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Used: 72
+X-Frame-Options: deny
+Content-Security-Policy: default-src 'none'
+
+{"id":220672974,"node_id":"MDEwOlJlcG9zaXRvcnkyMjA2NzI5NzQ=","name":"test_repo","full_name":"go-gitea/test_repo","private":false,"owner":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/go-gitea/test_repo","description":"Test repository for testing migration from github to gitea","fork":false,"url":"https://api.github.com/repos/go-gitea/test_repo","forks_url":"https://api.github.com/repos/go-gitea/test_repo/forks","keys_url":"https://api.github.com/repos/go-gitea/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/go-gitea/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/go-gitea/test_repo/teams","hooks_url":"https://api.github.com/repos/go-gitea/test_repo/hooks","issue_events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/go-gitea/test_repo/events","assignees_url":"https://api.github.com/repos/go-gitea/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/go-gitea/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/go-gitea/test_repo/tags","blobs_url":"https://api.github.com/repos/go-gitea/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/go-gitea/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/go-gitea/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/go-gitea/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/go-gitea/test_repo/languages","stargazers_url":"https://api.github.com/repos/go-gitea/test_repo/stargazers","contributors_url":"https://api.github.com/repos/go-gitea/test_repo/contributors","subscribers_url":"https://api.github.com/repos/go-gitea/test_repo/subscribers","subscription_url":"https://api.github.com/repos/go-gitea/test_repo/subscription","commits_url":"https://api.github.com/repos/go-gitea/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/go-gitea/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/go-gitea/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/go-gitea/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/go-gitea/test_repo/merges","archive_url":"https://api.github.com/repos/go-gitea/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/go-gitea/test_repo/downloads","issues_url":"https://api.github.com/repos/go-gitea/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/go-gitea/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/go-gitea/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/go-gitea/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/go-gitea/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/go-gitea/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/go-gitea/test_repo/deployments","created_at":"2019-11-09T16:49:20Z","updated_at":"2023-03-02T14:02:26Z","pushed_at":"2019-11-12T21:54:19Z","git_url":"git://github.com/go-gitea/test_repo.git","ssh_url":"git@github.com:go-gitea/test_repo.git","clone_url":"https://github.com/go-gitea/test_repo.git","svn_url":"https://github.com/go-gitea/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":1,"stargazers_count":3,"watchers_count":3,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":6,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["gitea"],"visibility":"public","forks":6,"open_issues":2,"watchers":3,"default_branch":"master","permissions":{"admin":false,"maintain":false,"push":false,"triage":false,"pull":true},"temp_clone_token":"","custom_properties":{},"organization":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"network_count":6,"subscribers_count":6}
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
new file mode 100644
index 0000000000..f1f9afee15
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
@@ -0,0 +1,24 @@
+Cache-Control: private, max-age=60, s-maxage=60
+X-Ratelimit-Remaining: 4923
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Accepted-Oauth-Scopes: repo
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Resource: core
+Content-Security-Policy: default-src 'none'
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Used: 77
+Access-Control-Allow-Origin: *
+X-Xss-Protection: 0
+X-Github-Request-Id: C7CC:3118FC:3F5F8DA:403618E:6729E6B6
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Type: application/json; charset=utf-8
+Etag: W/"07b6d56c5fdc728f96fceef3d45d26b4ebac96ef5138156668055f7d496c9a75"
+X-Oauth-Scopes:
+X-Ratelimit-Limit: 5000
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Frame-Options: deny
+X-Content-Type-Options: nosniff
+
+[{"id":55441655,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0MTY1NQ==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2019-11-12T20:22:13Z"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=2&per_page=2
similarity index 65%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=2&per_page=2
index 2af575abc9..fe993d3c3b 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=2&per_page=2
@@ -1,26 +1,26 @@
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
X-Github-Api-Version-Selected: 2022-11-28
-Content-Security-Policy: default-src 'none'
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Content-Length: 2
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
-X-Ratelimit-Remaining: 4875
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Github-Request-Id: E80A:118F3A:20652B:1E80D9:689B401D
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes: repo
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Reset: 1755007969
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Frame-Options: deny
Cache-Control: private, max-age=60, s-maxage=60
-X-Ratelimit-Used: 125
-X-Ratelimit-Resource: core
-Access-Control-Allow-Origin: *
-X-Content-Type-Options: nosniff
+Link: ; rel="prev", ; rel="last", ; rel="first"
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Remaining: 4922
+X-Ratelimit-Reset: 1730800941
X-Xss-Protection: 0
+X-Github-Request-Id: C7CC:3118FC:3F5FA7C:403633C:6729E6B6
+X-Oauth-Scopes:
+X-Ratelimit-Used: 78
+X-Frame-Options: deny
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Accepted-Oauth-Scopes: repo
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Access-Control-Allow-Origin: *
+Content-Security-Policy: default-src 'none'
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created
new file mode 100644
index 0000000000..61867c5ae6
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created
@@ -0,0 +1,24 @@
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Remaining: 4917
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Content-Type: application/json; charset=utf-8
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Used: 83
+X-Content-Type-Options: nosniff
+X-Github-Request-Id: C7CC:3118FC:3F60409:4036CD0:6729E6B8
+Etag: W/"5f4d715f2578719997e324fe7b29e7eeeec048288237b44d4f320666514813ad"
+X-Accepted-Oauth-Scopes:
+X-Frame-Options: deny
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Access-Control-Allow-Origin: *
+Content-Security-Policy: default-src 'none'
+Cache-Control: private, max-age=60, s-maxage=60
+X-Oauth-Scopes:
+
+[{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553111966","html_url":"https://github.com/go-gitea/test_repo/issues/2#issuecomment-553111966","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2","id":553111966,"node_id":"MDEyOklzc3VlQ29tbWVudDU1MzExMTk2Ng==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2019-11-12T21:00:13Z","updated_at":"2019-11-12T21:00:13Z","author_association":"MEMBER","body":"This is a comment","reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553111966/reactions","total_count":1,"+1":1,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null},{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553138856","html_url":"https://github.com/go-gitea/test_repo/issues/2#issuecomment-553138856","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2","id":553138856,"node_id":"MDEyOklzc3VlQ29tbWVudDU1MzEzODg1Ng==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2019-11-12T22:07:14Z","updated_at":"2019-11-12T22:07:14Z","author_association":"MEMBER","body":"A second comment","reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments/553138856/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2
new file mode 100644
index 0000000000..bb9dea395c
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2
@@ -0,0 +1,25 @@
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Xss-Protection: 0
+X-Accepted-Oauth-Scopes: repo
+Link: ; rel="next", ; rel="last"
+X-Ratelimit-Remaining: 4921
+X-Ratelimit-Reset: 1730800941
+X-Github-Request-Id: C7CC:3118FC:3F5FC1A:40364D4:6729E6B6
+Content-Security-Policy: default-src 'none'
+X-Oauth-Scopes:
+X-Ratelimit-Used: 79
+X-Frame-Options: deny
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Api-Version-Selected: 2022-11-28
+Content-Type: application/json; charset=utf-8
+Etag: W/"27408cb5dd95878d6267de226341c84fd1d2c49695867baecf930579608e16a5"
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Content-Type-Options: nosniff
+Cache-Control: private, max-age=60, s-maxage=60
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Limit: 5000
+Access-Control-Allow-Origin: *
+
+[{"id":55445108,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTEwOA==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"heart","created_at":"2019-11-12T21:02:05Z"},{"id":55445150,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE1MA==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"laugh","created_at":"2019-11-12T21:02:35Z"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=2&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=2&per_page=2
new file mode 100644
index 0000000000..e59fc93546
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=2&per_page=2
@@ -0,0 +1,25 @@
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Resource: core
+X-Frame-Options: deny
+Etag: W/"cb64a4e91ab70a1ab9fe2f77cdbf7452120169f4c2cce397014efe94cc0d60bb"
+Link: ; rel="prev", ; rel="next", ; rel="last", ; rel="first"
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Oauth-Scopes:
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Content-Type-Options: nosniff
+Content-Security-Policy: default-src 'none'
+Content-Type: application/json; charset=utf-8
+Cache-Control: private, max-age=60, s-maxage=60
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Request-Id: C7CC:3118FC:3F5FDBD:4036670:6729E6B7
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+Access-Control-Allow-Origin: *
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Remaining: 4920
+X-Ratelimit-Used: 80
+X-Accepted-Oauth-Scopes: repo
+
+[{"id":55445169,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE2OQ==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"-1","created_at":"2019-11-12T21:02:47Z"},{"id":55445177,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE3Nw==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"confused","created_at":"2019-11-12T21:02:52Z"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=3&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=3&per_page=2
new file mode 100644
index 0000000000..57f03c8a64
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=3&per_page=2
@@ -0,0 +1,25 @@
+Etag: W/"17b0dca978a885d2234548248a7d5c22264c161b73e28c5cd144e33a24dd9ed4"
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Used: 81
+Content-Security-Policy: default-src 'none'
+Content-Type: application/json; charset=utf-8
+X-Oauth-Scopes:
+Link: ; rel="prev", ; rel="first"
+X-Ratelimit-Limit: 5000
+X-Frame-Options: deny
+X-Content-Type-Options: nosniff
+X-Accepted-Oauth-Scopes: repo
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Xss-Protection: 0
+Access-Control-Allow-Origin: *
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Cache-Control: private, max-age=60, s-maxage=60
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Remaining: 4919
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Github-Request-Id: C7CC:3118FC:3F5FF70:403681F:6729E6B7
+
+[{"id":55445188,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTE4OA==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"hooray","created_at":"2019-11-12T21:02:58Z"},{"id":55445441,"node_id":"MDEzOklzc3VlUmVhY3Rpb241NTQ0NTQ0MQ==","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?u=7c1ba931adbdd9bab5be1a41d244425d463568cd&v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2019-11-12T21:06:04Z"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=4&per_page=2
similarity index 65%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=4&per_page=2
index ecaafa82e4..f14d4ba904 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=4&per_page=2
@@ -1,26 +1,26 @@
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Frame-Options: deny
-X-Content-Type-Options: nosniff
-X-Accepted-Oauth-Scopes: repo
-X-Ratelimit-Reset: 1755007969
-Access-Control-Allow-Origin: *
-Content-Type: application/json; charset=utf-8
-Content-Length: 2
-Cache-Control: private, max-age=60, s-maxage=60
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Oauth-Scopes: public_repo, repo:status
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Oauth-Scopes:
X-Ratelimit-Limit: 5000
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
-X-Ratelimit-Remaining: 4874
-X-Ratelimit-Used: 126
+X-Ratelimit-Remaining: 4918
+X-Ratelimit-Reset: 1730800941
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Content-Type-Options: nosniff
+X-Github-Request-Id: C7CC:3118FC:3F60229:4036AE8:6729E6B8
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
+Link: ; rel="prev", ; rel="last", ; rel="first"
+X-Accepted-Oauth-Scopes: repo
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Used: 82
+X-Frame-Options: deny
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+Cache-Control: private, max-age=60, s-maxage=60
+X-Github-Api-Version-Selected: 2022-11-28
X-Ratelimit-Resource: core
+Access-Control-Allow-Origin: *
X-Xss-Protection: 0
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Type: application/json; charset=utf-8
+Content-Length: 2
Content-Security-Policy: default-src 'none'
-X-Github-Request-Id: E80A:118F3A:206814:1E83B2:689B401E
-X-Github-Api-Version-Selected: 2022-11-28
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
index a900075c9b..a7a105b3e7 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
@@ -1,26 +1,25 @@
-Content-Type: application/json; charset=utf-8
-Content-Length: 2
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Used: 88
X-Ratelimit-Resource: core
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Github-Request-Id: E80A:118F3A:20758C:1E9080:689B401F
-X-Oauth-Scopes: public_repo, repo:status
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Reset: 1755007969
-Access-Control-Allow-Origin: *
-Content-Security-Policy: default-src 'none'
-Cache-Control: private, max-age=60, s-maxage=60
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Remaining: 4870
-X-Ratelimit-Used: 130
X-Content-Type-Options: nosniff
+Content-Security-Policy: default-src 'none'
+X-Github-Request-Id: C7CC:3118FC:3F60B4F:403741E:6729E6BA
+Content-Type: application/json; charset=utf-8
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Reset: 1730800941
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Cache-Control: private, max-age=60, s-maxage=60
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+Access-Control-Allow-Origin: *
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Frame-Options: deny
X-Xss-Protection: 0
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
+X-Oauth-Scopes:
X-Accepted-Oauth-Scopes: repo
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Frame-Options: deny
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Remaining: 4912
+Content-Length: 2
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=1&per_page=2
new file mode 100644
index 0000000000..f5398c3a9f
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=1&per_page=2
@@ -0,0 +1,24 @@
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+Etag: W/"dc9d10e1714eadc1507466c7d11d2dd84ae539a378835f8763b9948da44b22e1"
+X-Accepted-Oauth-Scopes: repo
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Access-Control-Allow-Origin: *
+X-Frame-Options: deny
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Reset: 1730800941
+Content-Type: application/json; charset=utf-8
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Remaining: 4911
+X-Ratelimit-Used: 89
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Content-Type-Options: nosniff
+X-Xss-Protection: 0
+Content-Security-Policy: default-src 'none'
+X-Github-Request-Id: C7CC:3118FC:3F60D0F:40375F2:6729E6BB
+Cache-Control: private, max-age=60, s-maxage=60
+X-Oauth-Scopes:
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Resource: core
+
+[{"id":59496724,"node_id":"MDEzOklzc3VlUmVhY3Rpb241OTQ5NjcyNA==","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"heart","created_at":"2020-01-10T08:31:30Z"},{"id":59496731,"node_id":"MDEzOklzc3VlUmVhY3Rpb241OTQ5NjczMQ==","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2020-01-10T08:31:39Z"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F7%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=2&per_page=2
similarity index 65%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F7%2Freactions%3Fpage=1&per_page=2
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=2&per_page=2
index 694c612697..79b506ea55 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F7%2Freactions%3Fpage=1&per_page=2
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=2&per_page=2
@@ -1,26 +1,26 @@
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Oauth-Scopes:
X-Accepted-Oauth-Scopes: repo
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Resource: core
-Access-Control-Allow-Origin: *
-X-Frame-Options: deny
-Cache-Control: private, max-age=60, s-maxage=60
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
-X-Ratelimit-Reset: 1755007969
-X-Xss-Protection: 0
-X-Github-Request-Id: E80A:118F3A:207936:1E936E:689B4020
-X-Oauth-Scopes: public_repo, repo:status
-X-Ratelimit-Remaining: 4869
+X-Ratelimit-Limit: 5000
Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Content-Length: 2
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
+X-Ratelimit-Remaining: 4910
+X-Ratelimit-Reset: 1730800941
+X-Frame-Options: deny
Content-Type: application/json; charset=utf-8
-Content-Length: 2
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Used: 131
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Link: ; rel="prev", ; rel="last", ; rel="first"
+X-Xss-Protection: 0
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Request-Id: C7CC:3118FC:3F60EE6:40377B6:6729E6BB
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Used: 90
+X-Ratelimit-Resource: core
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Github-Api-Version-Selected: 2022-11-28
+Access-Control-Allow-Origin: *
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=1&per_page=100
new file mode 100644
index 0000000000..b55068a718
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=1&per_page=100
@@ -0,0 +1,24 @@
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Accepted-Oauth-Scopes:
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Used: 84
+X-Ratelimit-Resource: core
+X-Content-Type-Options: nosniff
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: W/"d2410fc0792a61666c06ed757aa53a273165d4f14f7d5259095b7a4f3a959121"
+X-Xss-Protection: 0
+Content-Security-Policy: default-src 'none'
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Oauth-Scopes:
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Remaining: 4916
+X-Frame-Options: deny
+X-Github-Request-Id: C7CC:3118FC:3F60583:4036E51:6729E6B9
+Content-Type: application/json; charset=utf-8
+X-Ratelimit-Reset: 1730800941
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Access-Control-Allow-Origin: *
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+
+[{"id":55446208,"node_id":"MDIwOklzc3VlQ29tbWVudFJlYWN0aW9uNTU0NDYyMDg=","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"content":"+1","created_at":"2019-11-12T21:13:22Z"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=2&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=2&per_page=100
new file mode 100644
index 0000000000..1e46f438e5
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=2&per_page=100
@@ -0,0 +1,26 @@
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+Cache-Control: private, max-age=60, s-maxage=60
+X-Ratelimit-Used: 85
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Request-Id: C7CC:3118FC:3F606F2:4036FB5:6729E6B9
+X-Xss-Protection: 0
+Content-Type: application/json; charset=utf-8
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
+X-Oauth-Scopes:
+Link: ; rel="prev", ; rel="last", ; rel="first"
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Resource: core
+X-Frame-Options: deny
+Content-Length: 2
+X-Accepted-Oauth-Scopes:
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Remaining: 4915
+X-Ratelimit-Reset: 1730800941
+Access-Control-Allow-Origin: *
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Content-Type-Options: nosniff
+Content-Security-Policy: default-src 'none'
+
+[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2Fcomments%2F3164123494%2Freactions%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage=1&per_page=100
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2Fcomments%2F3164123494%2Freactions%3Fpage=1&per_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage=1&per_page=100
index 4ee1c6bc52..ac446b3586 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2Fcomments%2F3164123494%2Freactions%3Fpage=1&per_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage=1&per_page=100
@@ -1,26 +1,25 @@
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Content-Security-Policy: default-src 'none'
-Content-Type: application/json; charset=utf-8
-X-Oauth-Scopes: public_repo, repo:status
-X-Ratelimit-Remaining: 4872
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Remaining: 4914
X-Frame-Options: deny
X-Xss-Protection: 0
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Resource: core
+X-Content-Type-Options: nosniff
+Content-Type: application/json; charset=utf-8
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
+X-Accepted-Oauth-Scopes:
X-Github-Api-Version-Selected: 2022-11-28
X-Ratelimit-Limit: 5000
-X-Ratelimit-Used: 128
-X-Content-Type-Options: nosniff
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Cache-Control: private, max-age=60, s-maxage=60
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Resource: core
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Github-Request-Id: E80A:118F3A:206DA5:1E890E:689B401E
Content-Length: 2
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
-X-Accepted-Oauth-Scopes:
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
Access-Control-Allow-Origin: *
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Content-Security-Policy: default-src 'none'
+X-Github-Request-Id: C7CC:3118FC:3F60858:4037116:6729E6B9
+Cache-Control: private, max-age=60, s-maxage=60
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Oauth-Scopes:
+X-Ratelimit-Used: 86
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%3Fdirection=asc&page=1&per_page=2&sort=created&state=all b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
new file mode 100644
index 0000000000..80d2f90dbc
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
@@ -0,0 +1,25 @@
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Type: application/json; charset=utf-8
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Resource: core
+Access-Control-Allow-Origin: *
+X-Frame-Options: deny
+X-Xss-Protection: 0
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: W/"40580a89c26a3f7793cea4e59315e52e1106be32ded27b3ab822b7d7f74a1ecf"
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+Link: ; rel="next", ; rel="last"
+X-Ratelimit-Reset: 1730800941
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Content-Type-Options: nosniff
+X-Oauth-Scopes:
+X-Accepted-Oauth-Scopes: repo
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Remaining: 4924
+X-Ratelimit-Used: 76
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Content-Security-Policy: default-src 'none'
+X-Github-Request-Id: C7CC:3118FC:3F5F5B9:4035E6C:6729E6B5
+
+[{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/1","repository_url":"https://api.github.com/repos/go-gitea/test_repo","labels_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/labels{/name}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/comments","events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/events","html_url":"https://github.com/go-gitea/test_repo/issues/1","id":520479843,"node_id":"MDU6SXNzdWU1MjA0Nzk4NDM=","number":1,"title":"Please add an animated gif icon to the merge button","user":{"login":"guillep2k","id":18600385,"node_id":"MDQ6VXNlcjE4NjAwMzg1","avatar_url":"https://avatars.githubusercontent.com/u/18600385?v=4","gravatar_id":"","url":"https://api.github.com/users/guillep2k","html_url":"https://github.com/guillep2k","followers_url":"https://api.github.com/users/guillep2k/followers","following_url":"https://api.github.com/users/guillep2k/following{/other_user}","gists_url":"https://api.github.com/users/guillep2k/gists{/gist_id}","starred_url":"https://api.github.com/users/guillep2k/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/guillep2k/subscriptions","organizations_url":"https://api.github.com/users/guillep2k/orgs","repos_url":"https://api.github.com/users/guillep2k/repos","events_url":"https://api.github.com/users/guillep2k/events{/privacy}","received_events_url":"https://api.github.com/users/guillep2k/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":1667254252,"node_id":"MDU6TGFiZWwxNjY3MjU0MjUy","url":"https://api.github.com/repos/go-gitea/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"},{"id":1667254261,"node_id":"MDU6TGFiZWwxNjY3MjU0MjYx","url":"https://api.github.com/repos/go-gitea/test_repo/labels/good%20first%20issue","name":"good first issue","color":"7057ff","default":true,"description":"Good for newcomers"}],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1","html_url":"https://github.com/go-gitea/test_repo/milestone/1","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1/labels","id":4839941,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0MQ==","number":1,"title":"1.0.0","description":"Milestone 1.0.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":1,"state":"closed","created_at":"2019-11-12T19:37:08Z","updated_at":"2019-11-12T21:56:17Z","due_on":"2019-11-11T08:00:00Z","closed_at":"2019-11-12T19:45:49Z"},"comments":0,"created_at":"2019-11-09T17:00:29Z","updated_at":"2019-11-12T20:29:53Z","closed_at":"2019-11-12T20:22:22Z","author_association":"MEMBER","active_lock_reason":null,"body":"I just want the merge button to hurt my eyes a little. 😠","closed_by":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/reactions","total_count":1,"+1":1,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/go-gitea/test_repo/issues/1/timeline","performed_via_github_app":null,"state_reason":"completed"},{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/2","repository_url":"https://api.github.com/repos/go-gitea/test_repo","labels_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/labels{/name}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/comments","events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/events","html_url":"https://github.com/go-gitea/test_repo/issues/2","id":521799485,"node_id":"MDU6SXNzdWU1MjE3OTk0ODU=","number":2,"title":"Test issue","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[{"id":1667254257,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU3","url":"https://api.github.com/repos/go-gitea/test_repo/labels/duplicate","name":"duplicate","color":"cfd3d7","default":true,"description":"This issue or pull request already exists"}],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2","html_url":"https://github.com/go-gitea/test_repo/milestone/2","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2/labels","id":4839942,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0Mg==","number":2,"title":"1.1.0","description":"Milestone 1.1.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":0,"closed_issues":2,"state":"closed","created_at":"2019-11-12T19:37:25Z","updated_at":"2019-11-12T21:39:27Z","due_on":"2019-11-12T08:00:00Z","closed_at":"2019-11-12T19:45:46Z"},"comments":2,"created_at":"2019-11-12T21:00:06Z","updated_at":"2019-11-12T22:07:14Z","closed_at":"2019-11-12T21:01:31Z","author_association":"MEMBER","active_lock_reason":null,"body":"This is test issue 2, do not touch!","closed_by":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/reactions","total_count":6,"+1":1,"-1":1,"laugh":1,"hooray":1,"confused":1,"heart":1,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/go-gitea/test_repo/issues/2/timeline","performed_via_github_app":null,"state_reason":"completed"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Flabels%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Flabels%3Fpage=1&per_page=100
new file mode 100644
index 0000000000..f1d483aacb
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Flabels%3Fpage=1&per_page=100
@@ -0,0 +1,24 @@
+X-Ratelimit-Reset: 1730800941
+Access-Control-Allow-Origin: *
+Content-Security-Policy: default-src 'none'
+X-Github-Api-Version-Selected: 2022-11-28
+Etag: W/"01cc307b238564f2a086999fed53e0d5c880b8ec1d8d2256d99188ff47ff0ea0"
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Used: 74
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Ratelimit-Limit: 5000
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Xss-Protection: 0
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+Cache-Control: private, max-age=60, s-maxage=60
+X-Oauth-Scopes:
+X-Accepted-Oauth-Scopes: repo
+X-Ratelimit-Remaining: 4926
+X-Frame-Options: deny
+X-Content-Type-Options: nosniff
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Type: application/json; charset=utf-8
+X-Github-Request-Id: C7CC:3118FC:3F5F29C:4035B65:6729E6B4
+
+[{"id":1667254252,"node_id":"MDU6TGFiZWwxNjY3MjU0MjUy","url":"https://api.github.com/repos/go-gitea/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"},{"id":1667254254,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU0","url":"https://api.github.com/repos/go-gitea/test_repo/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"},{"id":1667254257,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU3","url":"https://api.github.com/repos/go-gitea/test_repo/labels/duplicate","name":"duplicate","color":"cfd3d7","default":true,"description":"This issue or pull request already exists"},{"id":1667254260,"node_id":"MDU6TGFiZWwxNjY3MjU0MjYw","url":"https://api.github.com/repos/go-gitea/test_repo/labels/enhancement","name":"enhancement","color":"a2eeef","default":true,"description":"New feature or request"},{"id":1667254261,"node_id":"MDU6TGFiZWwxNjY3MjU0MjYx","url":"https://api.github.com/repos/go-gitea/test_repo/labels/good%20first%20issue","name":"good first issue","color":"7057ff","default":true,"description":"Good for newcomers"},{"id":1667254265,"node_id":"MDU6TGFiZWwxNjY3MjU0MjY1","url":"https://api.github.com/repos/go-gitea/test_repo/labels/help%20wanted","name":"help wanted","color":"008672","default":true,"description":"Extra attention is needed"},{"id":1667254269,"node_id":"MDU6TGFiZWwxNjY3MjU0MjY5","url":"https://api.github.com/repos/go-gitea/test_repo/labels/invalid","name":"invalid","color":"e4e669","default":true,"description":"This doesn't seem right"},{"id":1667254273,"node_id":"MDU6TGFiZWwxNjY3MjU0Mjcz","url":"https://api.github.com/repos/go-gitea/test_repo/labels/question","name":"question","color":"d876e3","default":true,"description":"Further information is requested"},{"id":1667254276,"node_id":"MDU6TGFiZWwxNjY3MjU0Mjc2","url":"https://api.github.com/repos/go-gitea/test_repo/labels/wontfix","name":"wontfix","color":"ffffff","default":true,"description":"This will not be worked on"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all
new file mode 100644
index 0000000000..50b90ad4ab
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all
@@ -0,0 +1,24 @@
+X-Ratelimit-Remaining: 4927
+X-Ratelimit-Used: 73
+X-Frame-Options: deny
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Oauth-Scopes:
+X-Accepted-Oauth-Scopes: repo
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Access-Control-Allow-Origin: *
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Content-Type-Options: nosniff
+Content-Type: application/json; charset=utf-8
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Resource: core
+X-Github-Request-Id: C7CC:3118FC:3F5F16C:40359F7:6729E6B4
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Reset: 1730800941
+Content-Security-Policy: default-src 'none'
+X-Github-Api-Version-Selected: 2022-11-28
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: W/"d6d673f0622636217ee3df16cdabbfea8402d3e8d1abbabe007108267b01f3e9"
+
+[{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1","html_url":"https://github.com/go-gitea/test_repo/milestone/1","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1/labels","id":4839941,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0MQ==","number":1,"title":"1.0.0","description":"Milestone 1.0.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":1,"state":"closed","created_at":"2019-11-12T19:37:08Z","updated_at":"2019-11-12T21:56:17Z","due_on":"2019-11-11T08:00:00Z","closed_at":"2019-11-12T19:45:49Z"},{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2","html_url":"https://github.com/go-gitea/test_repo/milestone/2","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2/labels","id":4839942,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0Mg==","number":2,"title":"1.1.0","description":"Milestone 1.1.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":0,"closed_issues":2,"state":"closed","created_at":"2019-11-12T19:37:25Z","updated_at":"2019-11-12T21:39:27Z","due_on":"2019-11-12T08:00:00Z","closed_at":"2019-11-12T19:45:46Z"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Frequested_reviewers%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
similarity index 75%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Frequested_reviewers%3Fper_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
index f3bef5c3bd..1b4481f890 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Frequested_reviewers%3Fper_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
@@ -1,25 +1,24 @@
-X-Ratelimit-Remaining: 4958
-X-Content-Type-Options: nosniff
-Content-Security-Policy: default-src 'none'
-X-Oauth-Scopes: public_repo, repo:status
-X-Accepted-Oauth-Scopes:
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; format=json
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Used: 42
-X-Ratelimit-Resource: core
-Etag: W/"bf72ef7e37aa7c7a418e89035db9d7f23b969af0705f1e9c7ee692aad6c272f7"
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Access-Control-Allow-Origin: *
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Frame-Options: deny
-X-Xss-Protection: 0
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Cache-Control: private, max-age=60, s-maxage=60
-X-Github-Request-Id: F852:1553BC:10F18A:FD450:689B3DFF
-Content-Type: application/json; charset=utf-8
+Etag: W/"21730161122bd4f229e886dd4a85b45fa575182d6dcef7aa0016a5d21353c9ab"
+X-Oauth-Scopes:
+X-Github-Media-Type: github.v3; format=json
+Access-Control-Allow-Origin: *
+X-Frame-Options: deny
+X-Content-Type-Options: nosniff
+X-Xss-Protection: 0
+X-Github-Request-Id: C7CC:3118FC:3F6187A:4038171:6729E6BD
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Github-Api-Version-Selected: 2022-11-28
X-Ratelimit-Limit: 5000
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Content-Security-Policy: default-src 'none'
+Content-Type: application/json; charset=utf-8
+Cache-Control: private, max-age=60, s-maxage=60
+X-Accepted-Oauth-Scopes:
+X-Ratelimit-Remaining: 4905
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Used: 95
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
{"users":[],"teams":[]}
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Freviews%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page=100
similarity index 75%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Freviews%3Fper_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page=100
index fea2907aa4..435e1a0ee0 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Freviews%3Fper_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page=100
@@ -1,26 +1,25 @@
-X-Xss-Protection: 0
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Oauth-Scopes: public_repo, repo:status
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Media-Type: github.v3; format=json
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Github-Request-Id: F852:1553BC:10EEC0:FD190:689B3DFE
-Content-Type: application/json; charset=utf-8
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Used: 92
X-Frame-Options: deny
-Content-Security-Policy: default-src 'none'
+X-Content-Type-Options: nosniff
Content-Length: 2
-X-Accepted-Oauth-Scopes:
+X-Oauth-Scopes:
+X-Ratelimit-Remaining: 4908
+X-Ratelimit-Reset: 1730800941
+X-Github-Request-Id: C7CC:3118FC:3F612D6:4037BAC:6729E6BC
+Etag: "450a1c087fec81e5b86092ff5372c3db8ca834c1e23c03c6b06ecca33cefd665"
X-Github-Api-Version-Selected: 2022-11-28
X-Ratelimit-Limit: 5000
-X-Ratelimit-Remaining: 4959
-X-Ratelimit-Used: 41
X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Access-Control-Allow-Origin: *
Cache-Control: private, max-age=60, s-maxage=60
-Etag: "bb321a3b596949885b60e203a5ea335475c7ac9e2364d10f463cb934490c25a5"
-X-Ratelimit-Reset: 1755007969
+X-Accepted-Oauth-Scopes:
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Content-Type-Options: nosniff
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Security-Policy: default-src 'none'
+Content-Type: application/json; charset=utf-8
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page=100
new file mode 100644
index 0000000000..389c1b7567
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page=100
@@ -0,0 +1,25 @@
+Content-Length: 2
+X-Frame-Options: deny
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Used: 93
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Content-Type: application/json; charset=utf-8
+X-Accepted-Oauth-Scopes:
+X-Xss-Protection: 0
+X-Oauth-Scopes:
+X-Ratelimit-Remaining: 4907
+X-Ratelimit-Reset: 1730800941
+Access-Control-Allow-Origin: *
+X-Content-Type-Options: nosniff
+Content-Security-Policy: default-src 'none'
+Cache-Control: private, max-age=60, s-maxage=60
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Github-Request-Id: C7CC:3118FC:3F614AA:4037DB7:6729E6BD
+X-Github-Media-Type: github.v3; format=json
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Etag: "450a1c087fec81e5b86092ff5372c3db8ca834c1e23c03c6b06ecca33cefd665"
+
+[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315861440%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315861440%2Fcomments%3Fper_page=100
new file mode 100644
index 0000000000..e52428a5af
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315861440%2Fcomments%3Fper_page=100
@@ -0,0 +1,25 @@
+Content-Type: application/json; charset=utf-8
+Cache-Control: private, max-age=60, s-maxage=60
+X-Ratelimit-Reset: 1730800941
+X-Content-Type-Options: nosniff
+Content-Security-Policy: default-src 'none'
+X-Accepted-Oauth-Scopes:
+X-Xss-Protection: 0
+X-Github-Request-Id: C7CC:3118FC:3F61690:4037F90:6729E6BD
+Content-Length: 2
+X-Oauth-Scopes:
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Frame-Options: deny
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Access-Control-Allow-Origin: *
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+Etag: "450a1c087fec81e5b86092ff5372c3db8ca834c1e23c03c6b06ecca33cefd665"
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Remaining: 4906
+X-Ratelimit-Used: 94
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+
+[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100
new file mode 100644
index 0000000000..203c363ffa
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100
@@ -0,0 +1,24 @@
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: W/"e38ac3d6f3e77a469f9836bfa52a0b756b9ac8fdc4347530e1cb1072bbb77b46"
+X-Github-Media-Type: github.v3; format=json
+X-Frame-Options: deny
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Access-Control-Allow-Origin: *
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Accepted-Oauth-Scopes:
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Remaining: 4909
+X-Ratelimit-Reset: 1730800941
+Content-Security-Policy: default-src 'none'
+X-Github-Request-Id: C7CC:3118FC:3F6108F:403797A:6729E6BC
+Content-Type: application/json; charset=utf-8
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Resource: core
+X-Content-Type-Options: nosniff
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Oauth-Scopes:
+X-Ratelimit-Used: 91
+X-Xss-Protection: 0
+
+[{"id":315859956,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzE1ODU5OTU2","user":{"login":"jolheiser","id":42128690,"node_id":"MDQ6VXNlcjQyMTI4Njkw","avatar_url":"https://avatars.githubusercontent.com/u/42128690?u=0ee1052506846129445fa12a76cd9ad9d305de71&v=4","gravatar_id":"","url":"https://api.github.com/users/jolheiser","html_url":"https://github.com/jolheiser","followers_url":"https://api.github.com/users/jolheiser/followers","following_url":"https://api.github.com/users/jolheiser/following{/other_user}","gists_url":"https://api.github.com/users/jolheiser/gists{/gist_id}","starred_url":"https://api.github.com/users/jolheiser/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jolheiser/subscriptions","organizations_url":"https://api.github.com/users/jolheiser/orgs","repos_url":"https://api.github.com/users/jolheiser/repos","events_url":"https://api.github.com/users/jolheiser/events{/privacy}","received_events_url":"https://api.github.com/users/jolheiser/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315859956","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315859956"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"}},"submitted_at":"2019-11-12T21:35:24Z","commit_id":"076160cf0b039f13e5eff19619932d181269414b"},{"id":315860062,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzE1ODYwMDYy","user":{"login":"zeripath","id":1824502,"node_id":"MDQ6VXNlcjE4MjQ1MDI=","avatar_url":"https://avatars.githubusercontent.com/u/1824502?u=fcd8a9dba8714edf6ac3f87596eb72149911c720&v=4","gravatar_id":"","url":"https://api.github.com/users/zeripath","html_url":"https://github.com/zeripath","followers_url":"https://api.github.com/users/zeripath/followers","following_url":"https://api.github.com/users/zeripath/following{/other_user}","gists_url":"https://api.github.com/users/zeripath/gists{/gist_id}","starred_url":"https://api.github.com/users/zeripath/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zeripath/subscriptions","organizations_url":"https://api.github.com/users/zeripath/orgs","repos_url":"https://api.github.com/users/zeripath/repos","events_url":"https://api.github.com/users/zeripath/events{/privacy}","received_events_url":"https://api.github.com/users/zeripath/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315860062","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","author_association":"NONE","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315860062"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"}},"submitted_at":"2019-11-12T21:35:36Z","commit_id":"076160cf0b039f13e5eff19619932d181269414b"},{"id":315861440,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzE1ODYxNDQw","user":{"login":"lafriks","id":165205,"node_id":"MDQ6VXNlcjE2NTIwNQ==","avatar_url":"https://avatars.githubusercontent.com/u/165205?u=efe2335d2197f524c25caa7abdfcb90b77eb8d98&v=4","gravatar_id":"","url":"https://api.github.com/users/lafriks","html_url":"https://github.com/lafriks","followers_url":"https://api.github.com/users/lafriks/followers","following_url":"https://api.github.com/users/lafriks/following{/other_user}","gists_url":"https://api.github.com/users/lafriks/gists{/gist_id}","starred_url":"https://api.github.com/users/lafriks/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lafriks/subscriptions","organizations_url":"https://api.github.com/users/lafriks/orgs","repos_url":"https://api.github.com/users/lafriks/repos","events_url":"https://api.github.com/users/lafriks/events{/privacy}","received_events_url":"https://api.github.com/users/lafriks/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315861440","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/3#pullrequestreview-315861440"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"}},"submitted_at":"2019-11-12T21:38:00Z","commit_id":"076160cf0b039f13e5eff19619932d181269414b"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers%3Fper_page=100
similarity index 75%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers%3Fper_page=100
index 5d382a0ed1..676e326094 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers%3Fper_page=100
@@ -1,25 +1,24 @@
-Content-Type: application/json; charset=utf-8
-X-Oauth-Scopes: public_repo, repo:status
-Access-Control-Allow-Origin: *
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Content-Security-Policy: default-src 'none'
-Cache-Control: private, max-age=60, s-maxage=60
-X-Accepted-Oauth-Scopes:
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Resource: core
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-X-Frame-Options: deny
-X-Content-Type-Options: nosniff
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Github-Media-Type: github.v3; format=json
+X-Oauth-Scopes:
X-Ratelimit-Limit: 5000
-X-Ratelimit-Remaining: 4863
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Used: 137
+X-Ratelimit-Remaining: 4898
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
X-Xss-Protection: 0
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Etag: W/"bf72ef7e37aa7c7a418e89035db9d7f23b969af0705f1e9c7ee692aad6c272f7"
-X-Github-Request-Id: E80A:118F3A:208A8A:1EA3C0:689B4023
+Content-Security-Policy: default-src 'none'
+X-Github-Request-Id: C7CC:3118FC:3F623DF:4038CEB:6729E6C0
+Etag: W/"21730161122bd4f229e886dd4a85b45fa575182d6dcef7aa0016a5d21353c9ab"
+X-Accepted-Oauth-Scopes:
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Resource: core
+Access-Control-Allow-Origin: *
+X-Frame-Options: deny
+Content-Type: application/json; charset=utf-8
+Cache-Control: private, max-age=60, s-maxage=60
+X-Ratelimit-Used: 102
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Reset: 1730800941
+X-Content-Type-Options: nosniff
{"users":[],"teams":[]}
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338338740%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338338740%2Fcomments%3Fper_page=100
new file mode 100644
index 0000000000..48e5b2c3d9
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338338740%2Fcomments%3Fper_page=100
@@ -0,0 +1,24 @@
+Access-Control-Allow-Origin: *
+X-Content-Type-Options: nosniff
+Content-Security-Policy: default-src 'none'
+X-Ratelimit-Resource: core
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Accepted-Oauth-Scopes:
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Remaining: 4903
+X-Ratelimit-Reset: 1730800941
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Frame-Options: deny
+X-Github-Request-Id: C7CC:3118FC:3F61BAA:40384A5:6729E6BE
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: W/"ff77892df2ec7f6eb61416e0f384ce0a6e8fbbbc1287f5d09b1980ebe9856750"
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Used: 97
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Type: application/json; charset=utf-8
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Xss-Protection: 0
+X-Oauth-Scopes:
+
+[{"id":363017488,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDM2MzAxNzQ4OA==","url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363017488","pull_request_review_id":338338740,"diff_hunk":"@@ -1,2 +1,4 @@\n # test_repo\n Test repository for testing migration from github to gitea\n+","path":"README.md","position":3,"original_position":3,"commit_id":"2be9101c543658591222acbee3eb799edfc3853d","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"This is a good pull request.","created_at":"2020-01-04T05:33:06Z","updated_at":"2020-01-04T05:33:18Z","html_url":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363017488","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363017488"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363017488"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"original_commit_id":"2be9101c543658591222acbee3eb799edfc3853d","reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363017488/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0}}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338339651%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338339651%2Fcomments%3Fper_page=100
new file mode 100644
index 0000000000..4cc66424f0
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338339651%2Fcomments%3Fper_page=100
@@ -0,0 +1,25 @@
+X-Github-Api-Version-Selected: 2022-11-28
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Frame-Options: deny
+X-Content-Type-Options: nosniff
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: "450a1c087fec81e5b86092ff5372c3db8ca834c1e23c03c6b06ecca33cefd665"
+X-Github-Media-Type: github.v3; format=json
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Ratelimit-Reset: 1730800941
+X-Xss-Protection: 0
+X-Oauth-Scopes:
+X-Accepted-Oauth-Scopes:
+X-Ratelimit-Limit: 5000
+Access-Control-Allow-Origin: *
+Content-Length: 2
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Resource: core
+Content-Security-Policy: default-src 'none'
+X-Github-Request-Id: C7CC:3118FC:3F61EBA:40387A6:6729E6BF
+Content-Type: application/json; charset=utf-8
+X-Ratelimit-Remaining: 4901
+X-Ratelimit-Used: 99
+
+[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338349019%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338349019%2Fcomments%3Fper_page=100
new file mode 100644
index 0000000000..f13d4addc7
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338349019%2Fcomments%3Fper_page=100
@@ -0,0 +1,24 @@
+Etag: W/"f74a383d4c87ae26d218fc087bef2c41a12637dff81fd8642f59708adca1a14e"
+X-Ratelimit-Remaining: 4900
+X-Content-Type-Options: nosniff
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Reset: 1730800941
+X-Xss-Protection: 0
+Content-Type: application/json; charset=utf-8
+X-Oauth-Scopes:
+X-Accepted-Oauth-Scopes:
+X-Github-Api-Version-Selected: 2022-11-28
+Access-Control-Allow-Origin: *
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Frame-Options: deny
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Used: 100
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Github-Request-Id: C7CC:3118FC:3F62065:4038955:6729E6C0
+Content-Security-Policy: default-src 'none'
+Cache-Control: private, max-age=60, s-maxage=60
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+
+[{"id":363029944,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDM2MzAyOTk0NA==","url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363029944","pull_request_review_id":338349019,"diff_hunk":"@@ -19,3 +19,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE.\n+","path":"LICENSE","position":4,"original_position":4,"commit_id":"2be9101c543658591222acbee3eb799edfc3853d","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"test a single comment.","created_at":"2020-01-04T11:21:41Z","updated_at":"2020-01-04T11:21:41Z","html_url":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363029944","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363029944"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#discussion_r363029944"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"original_commit_id":"2be9101c543658591222acbee3eb799edfc3853d","reactions":{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments/363029944/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0}}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
new file mode 100644
index 0000000000..c4484e078a
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
@@ -0,0 +1,24 @@
+Cache-Control: private, max-age=60, s-maxage=60
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Xss-Protection: 0
+Content-Security-Policy: default-src 'none'
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Github-Media-Type: github.v3; format=json
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Reset: 1730800941
+X-Frame-Options: deny
+X-Github-Request-Id: C7CC:3118FC:3F619C6:40382C4:6729E6BE
+Access-Control-Allow-Origin: *
+X-Content-Type-Options: nosniff
+Content-Type: application/json; charset=utf-8
+X-Accepted-Oauth-Scopes:
+X-Ratelimit-Used: 96
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Etag: W/"a47623061be83c50d3932baf8c5961386d2d45c32c42b504122c9a1359efd515"
+X-Oauth-Scopes:
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Remaining: 4904
+
+[{"id":338338740,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzM4MzM4NzQw","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"APPROVED","html_url":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338338740","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338338740"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"submitted_at":"2020-01-04T05:33:18Z","commit_id":"2be9101c543658591222acbee3eb799edfc3853d"},{"id":338339651,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzM4MzM5NjUx","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Don't add more reviews","state":"CHANGES_REQUESTED","html_url":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338339651","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338339651"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"submitted_at":"2020-01-04T06:07:06Z","commit_id":"2be9101c543658591222acbee3eb799edfc3853d"},{"id":338349019,"node_id":"MDE3OlB1bGxSZXF1ZXN0UmV2aWV3MzM4MzQ5MDE5","user":{"login":"lunny","id":81045,"node_id":"MDQ6VXNlcjgxMDQ1","avatar_url":"https://avatars.githubusercontent.com/u/81045?u=99b64f0ca6ef63643c7583ab87dd31c52d28e673&v=4","gravatar_id":"","url":"https://api.github.com/users/lunny","html_url":"https://github.com/lunny","followers_url":"https://api.github.com/users/lunny/followers","following_url":"https://api.github.com/users/lunny/following{/other_user}","gists_url":"https://api.github.com/users/lunny/gists{/gist_id}","starred_url":"https://api.github.com/users/lunny/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lunny/subscriptions","organizations_url":"https://api.github.com/users/lunny/orgs","repos_url":"https://api.github.com/users/lunny/repos","events_url":"https://api.github.com/users/lunny/events{/privacy}","received_events_url":"https://api.github.com/users/lunny/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"","state":"COMMENTED","html_url":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338349019","pull_request_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","author_association":"MEMBER","_links":{"html":{"href":"https://github.com/go-gitea/test_repo/pull/4#pullrequestreview-338349019"},"pull_request":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"}},"submitted_at":"2020-01-04T11:21:41Z","commit_id":"2be9101c543658591222acbee3eb799edfc3853d"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260216729%2Freactions%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage=1&per_page=100
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260216729%2Freactions%3Fpage=1&per_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage=1&per_page=100
index 39a6e753b9..748ae93381 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260216729%2Freactions%3Fpage=1&per_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage=1&per_page=100
@@ -1,26 +1,25 @@
-Content-Type: application/json; charset=utf-8
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
-X-Github-Api-Version-Selected: 2022-11-28
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Frame-Options: deny
-X-Github-Request-Id: E80A:118F3A:2082EC:1E9C92:689B4022
-Content-Length: 2
-X-Ratelimit-Remaining: 4866
-X-Ratelimit-Resource: core
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
-Cache-Control: private, max-age=60, s-maxage=60
-X-Oauth-Scopes: public_repo, repo:status
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
X-Accepted-Oauth-Scopes:
+X-Ratelimit-Remaining: 4902
+X-Ratelimit-Reset: 1730800941
+X-Ratelimit-Resource: core
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Used: 134
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
Access-Control-Allow-Origin: *
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+X-Github-Request-Id: C7CC:3118FC:3F61D73:4038667:6729E6BF
+Content-Type: application/json; charset=utf-8
+X-Ratelimit-Limit: 5000
+Content-Length: 2
+Cache-Control: private, max-age=60, s-maxage=60
+X-Oauth-Scopes:
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Used: 98
+X-Frame-Options: deny
X-Content-Type-Options: nosniff
X-Xss-Protection: 0
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260221159%2Freactions%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage=1&per_page=100
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260221159%2Freactions%3Fpage=1&per_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage=1&per_page=100
index 1c80bb32d7..0b0ae88deb 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260221159%2Freactions%3Fpage=1&per_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage=1&per_page=100
@@ -1,26 +1,25 @@
-X-Ratelimit-Used: 136
-X-Frame-Options: deny
+X-Accepted-Oauth-Scopes:
+X-Ratelimit-Remaining: 4899
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Github-Request-Id: E80A:118F3A:20881D:1EA17C:689B4022
-Content-Type: application/json; charset=utf-8
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Content-Length: 2
+X-Ratelimit-Used: 101
Access-Control-Allow-Origin: *
+X-Frame-Options: deny
+X-Content-Type-Options: nosniff
+X-Xss-Protection: 0
Content-Security-Policy: default-src 'none'
Cache-Control: private, max-age=60, s-maxage=60
-X-Oauth-Scopes: public_repo, repo:status
-Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
+Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
+X-Oauth-Scopes:
X-Github-Api-Version-Selected: 2022-11-28
-X-Content-Type-Options: nosniff
-Content-Length: 2
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
-X-Ratelimit-Remaining: 4864
-X-Ratelimit-Reset: 1755007969
-X-Ratelimit-Resource: core
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Xss-Protection: 0
-X-Accepted-Oauth-Scopes:
X-Ratelimit-Limit: 5000
+X-Ratelimit-Reset: 1730800941
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Content-Type: application/json; charset=utf-8
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Resource: core
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Github-Request-Id: C7CC:3118FC:3F622AC:4038BA5:6729E6C0
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%3Fdirection=asc&page=1&per_page=2&sort=created&state=all b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
new file mode 100644
index 0000000000..30883cc283
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
@@ -0,0 +1,24 @@
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Used: 87
+X-Frame-Options: deny
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Remaining: 4913
+X-Ratelimit-Reset: 1730800941
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Security-Policy: default-src 'none'
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Oauth-Scopes:
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Resource: core
+Access-Control-Allow-Origin: *
+X-Github-Request-Id: C7CC:3118FC:3F6099B:403726B:6729E6BA
+X-Xss-Protection: 0
+Content-Type: application/json; charset=utf-8
+Cache-Control: private, max-age=60, s-maxage=60
+Etag: W/"fed37ab8ce0b78e713030ff3e601f540b7f71243ab788b477f6885119bd691c5"
+X-Accepted-Oauth-Scopes:
+X-Content-Type-Options: nosniff
+
+[{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3","id":340118745,"node_id":"MDExOlB1bGxSZXF1ZXN0MzQwMTE4NzQ1","html_url":"https://github.com/go-gitea/test_repo/pull/3","diff_url":"https://github.com/go-gitea/test_repo/pull/3.diff","patch_url":"https://github.com/go-gitea/test_repo/pull/3.patch","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/3","number":3,"state":"closed","locked":false,"title":"Update README.md","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"add warning to readme","created_at":"2019-11-12T21:21:43Z","updated_at":"2019-11-12T21:39:28Z","closed_at":"2019-11-12T21:39:27Z","merged_at":"2019-11-12T21:39:27Z","merge_commit_sha":"f32b0a9dfd09a60f616f29158f772cedd89942d2","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":1667254254,"node_id":"MDU6TGFiZWwxNjY3MjU0MjU0","url":"https://api.github.com/repos/go-gitea/test_repo/labels/documentation","name":"documentation","color":"0075ca","default":true,"description":"Improvements or additions to documentation"}],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2","html_url":"https://github.com/go-gitea/test_repo/milestone/2","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/2/labels","id":4839942,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0Mg==","number":2,"title":"1.1.0","description":"Milestone 1.1.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":0,"closed_issues":2,"state":"closed","created_at":"2019-11-12T19:37:25Z","updated_at":"2019-11-12T21:39:27Z","due_on":"2019-11-12T08:00:00Z","closed_at":"2019-11-12T19:45:46Z"},"draft":false,"commits_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/commits","review_comments_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/comments","review_comment_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/3/comments","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/076160cf0b039f13e5eff19619932d181269414b","head":{"label":"mrsdizzie:master","ref":"master","sha":"076160cf0b039f13e5eff19619932d181269414b","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"repo":{"id":221313794,"node_id":"MDEwOlJlcG9zaXRvcnkyMjEzMTM3OTQ=","name":"test_repo","full_name":"mrsdizzie/test_repo","private":false,"owner":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/mrsdizzie/test_repo","description":"Test repository for testing migration from github to gitea","fork":true,"url":"https://api.github.com/repos/mrsdizzie/test_repo","forks_url":"https://api.github.com/repos/mrsdizzie/test_repo/forks","keys_url":"https://api.github.com/repos/mrsdizzie/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mrsdizzie/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mrsdizzie/test_repo/teams","hooks_url":"https://api.github.com/repos/mrsdizzie/test_repo/hooks","issue_events_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/mrsdizzie/test_repo/events","assignees_url":"https://api.github.com/repos/mrsdizzie/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/mrsdizzie/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/tags","blobs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mrsdizzie/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/mrsdizzie/test_repo/languages","stargazers_url":"https://api.github.com/repos/mrsdizzie/test_repo/stargazers","contributors_url":"https://api.github.com/repos/mrsdizzie/test_repo/contributors","subscribers_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscribers","subscription_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscription","commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/mrsdizzie/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/mrsdizzie/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/mrsdizzie/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mrsdizzie/test_repo/merges","archive_url":"https://api.github.com/repos/mrsdizzie/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mrsdizzie/test_repo/downloads","issues_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/mrsdizzie/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/mrsdizzie/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/mrsdizzie/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mrsdizzie/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/mrsdizzie/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/mrsdizzie/test_repo/deployments","created_at":"2019-11-12T21:17:42Z","updated_at":"2019-11-12T21:18:46Z","pushed_at":"2019-11-12T21:53:39Z","git_url":"git://github.com/mrsdizzie/test_repo.git","ssh_url":"git@github.com:mrsdizzie/test_repo.git","clone_url":"https://github.com/mrsdizzie/test_repo.git","svn_url":"https://github.com/mrsdizzie/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":3,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"go-gitea:master","ref":"master","sha":"72866af952e98d02a73003501836074b286a78f6","user":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":220672974,"node_id":"MDEwOlJlcG9zaXRvcnkyMjA2NzI5NzQ=","name":"test_repo","full_name":"go-gitea/test_repo","private":false,"owner":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/go-gitea/test_repo","description":"Test repository for testing migration from github to gitea","fork":false,"url":"https://api.github.com/repos/go-gitea/test_repo","forks_url":"https://api.github.com/repos/go-gitea/test_repo/forks","keys_url":"https://api.github.com/repos/go-gitea/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/go-gitea/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/go-gitea/test_repo/teams","hooks_url":"https://api.github.com/repos/go-gitea/test_repo/hooks","issue_events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/go-gitea/test_repo/events","assignees_url":"https://api.github.com/repos/go-gitea/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/go-gitea/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/go-gitea/test_repo/tags","blobs_url":"https://api.github.com/repos/go-gitea/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/go-gitea/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/go-gitea/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/go-gitea/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/go-gitea/test_repo/languages","stargazers_url":"https://api.github.com/repos/go-gitea/test_repo/stargazers","contributors_url":"https://api.github.com/repos/go-gitea/test_repo/contributors","subscribers_url":"https://api.github.com/repos/go-gitea/test_repo/subscribers","subscription_url":"https://api.github.com/repos/go-gitea/test_repo/subscription","commits_url":"https://api.github.com/repos/go-gitea/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/go-gitea/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/go-gitea/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/go-gitea/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/go-gitea/test_repo/merges","archive_url":"https://api.github.com/repos/go-gitea/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/go-gitea/test_repo/downloads","issues_url":"https://api.github.com/repos/go-gitea/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/go-gitea/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/go-gitea/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/go-gitea/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/go-gitea/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/go-gitea/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/go-gitea/test_repo/deployments","created_at":"2019-11-09T16:49:20Z","updated_at":"2023-03-02T14:02:26Z","pushed_at":"2019-11-12T21:54:19Z","git_url":"git://github.com/go-gitea/test_repo.git","ssh_url":"git@github.com:go-gitea/test_repo.git","clone_url":"https://github.com/go-gitea/test_repo.git","svn_url":"https://github.com/go-gitea/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":1,"stargazers_count":3,"watchers_count":3,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":6,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["gitea"],"visibility":"public","forks":6,"open_issues":2,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/3"},"issue":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/3"},"comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/3/comments"},"review_comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/comments"},"review_comment":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/3/commits"},"statuses":{"href":"https://api.github.com/repos/go-gitea/test_repo/statuses/076160cf0b039f13e5eff19619932d181269414b"}},"author_association":"MEMBER","auto_merge":null,"active_lock_reason":null},{"url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4","id":340131577,"node_id":"MDExOlB1bGxSZXF1ZXN0MzQwMTMxNTc3","html_url":"https://github.com/go-gitea/test_repo/pull/4","diff_url":"https://github.com/go-gitea/test_repo/pull/4.diff","patch_url":"https://github.com/go-gitea/test_repo/pull/4.patch","issue_url":"https://api.github.com/repos/go-gitea/test_repo/issues/4","number":4,"state":"open","locked":false,"title":"Test branch","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"do not merge this PR","created_at":"2019-11-12T21:54:18Z","updated_at":"2020-01-04T11:30:01Z","closed_at":null,"merged_at":null,"merge_commit_sha":"565d1208f5fffdc1c5ae1a2436491eb9a5e4ebae","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":1667254252,"node_id":"MDU6TGFiZWwxNjY3MjU0MjUy","url":"https://api.github.com/repos/go-gitea/test_repo/labels/bug","name":"bug","color":"d73a4a","default":true,"description":"Something isn't working"}],"milestone":{"url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1","html_url":"https://github.com/go-gitea/test_repo/milestone/1","labels_url":"https://api.github.com/repos/go-gitea/test_repo/milestones/1/labels","id":4839941,"node_id":"MDk6TWlsZXN0b25lNDgzOTk0MQ==","number":1,"title":"1.0.0","description":"Milestone 1.0.0","creator":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"open_issues":1,"closed_issues":1,"state":"closed","created_at":"2019-11-12T19:37:08Z","updated_at":"2019-11-12T21:56:17Z","due_on":"2019-11-11T08:00:00Z","closed_at":"2019-11-12T19:45:49Z"},"draft":false,"commits_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/commits","review_comments_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/comments","review_comment_url":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/issues/4/comments","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/2be9101c543658591222acbee3eb799edfc3853d","head":{"label":"mrsdizzie:test-branch","ref":"test-branch","sha":"2be9101c543658591222acbee3eb799edfc3853d","user":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"repo":{"id":221313794,"node_id":"MDEwOlJlcG9zaXRvcnkyMjEzMTM3OTQ=","name":"test_repo","full_name":"mrsdizzie/test_repo","private":false,"owner":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/mrsdizzie/test_repo","description":"Test repository for testing migration from github to gitea","fork":true,"url":"https://api.github.com/repos/mrsdizzie/test_repo","forks_url":"https://api.github.com/repos/mrsdizzie/test_repo/forks","keys_url":"https://api.github.com/repos/mrsdizzie/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mrsdizzie/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mrsdizzie/test_repo/teams","hooks_url":"https://api.github.com/repos/mrsdizzie/test_repo/hooks","issue_events_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/mrsdizzie/test_repo/events","assignees_url":"https://api.github.com/repos/mrsdizzie/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/mrsdizzie/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/tags","blobs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mrsdizzie/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/mrsdizzie/test_repo/languages","stargazers_url":"https://api.github.com/repos/mrsdizzie/test_repo/stargazers","contributors_url":"https://api.github.com/repos/mrsdizzie/test_repo/contributors","subscribers_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscribers","subscription_url":"https://api.github.com/repos/mrsdizzie/test_repo/subscription","commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/mrsdizzie/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/mrsdizzie/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/mrsdizzie/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/mrsdizzie/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mrsdizzie/test_repo/merges","archive_url":"https://api.github.com/repos/mrsdizzie/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mrsdizzie/test_repo/downloads","issues_url":"https://api.github.com/repos/mrsdizzie/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/mrsdizzie/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/mrsdizzie/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/mrsdizzie/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mrsdizzie/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/mrsdizzie/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/mrsdizzie/test_repo/deployments","created_at":"2019-11-12T21:17:42Z","updated_at":"2019-11-12T21:18:46Z","pushed_at":"2019-11-12T21:53:39Z","git_url":"git://github.com/mrsdizzie/test_repo.git","ssh_url":"git@github.com:mrsdizzie/test_repo.git","clone_url":"https://github.com/mrsdizzie/test_repo.git","svn_url":"https://github.com/mrsdizzie/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":3,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":0,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"go-gitea:master","ref":"master","sha":"f32b0a9dfd09a60f616f29158f772cedd89942d2","user":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"repo":{"id":220672974,"node_id":"MDEwOlJlcG9zaXRvcnkyMjA2NzI5NzQ=","name":"test_repo","full_name":"go-gitea/test_repo","private":false,"owner":{"login":"go-gitea","id":12724356,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2","avatar_url":"https://avatars.githubusercontent.com/u/12724356?v=4","gravatar_id":"","url":"https://api.github.com/users/go-gitea","html_url":"https://github.com/go-gitea","followers_url":"https://api.github.com/users/go-gitea/followers","following_url":"https://api.github.com/users/go-gitea/following{/other_user}","gists_url":"https://api.github.com/users/go-gitea/gists{/gist_id}","starred_url":"https://api.github.com/users/go-gitea/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/go-gitea/subscriptions","organizations_url":"https://api.github.com/users/go-gitea/orgs","repos_url":"https://api.github.com/users/go-gitea/repos","events_url":"https://api.github.com/users/go-gitea/events{/privacy}","received_events_url":"https://api.github.com/users/go-gitea/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/go-gitea/test_repo","description":"Test repository for testing migration from github to gitea","fork":false,"url":"https://api.github.com/repos/go-gitea/test_repo","forks_url":"https://api.github.com/repos/go-gitea/test_repo/forks","keys_url":"https://api.github.com/repos/go-gitea/test_repo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/go-gitea/test_repo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/go-gitea/test_repo/teams","hooks_url":"https://api.github.com/repos/go-gitea/test_repo/hooks","issue_events_url":"https://api.github.com/repos/go-gitea/test_repo/issues/events{/number}","events_url":"https://api.github.com/repos/go-gitea/test_repo/events","assignees_url":"https://api.github.com/repos/go-gitea/test_repo/assignees{/user}","branches_url":"https://api.github.com/repos/go-gitea/test_repo/branches{/branch}","tags_url":"https://api.github.com/repos/go-gitea/test_repo/tags","blobs_url":"https://api.github.com/repos/go-gitea/test_repo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/go-gitea/test_repo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/go-gitea/test_repo/git/refs{/sha}","trees_url":"https://api.github.com/repos/go-gitea/test_repo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/go-gitea/test_repo/statuses/{sha}","languages_url":"https://api.github.com/repos/go-gitea/test_repo/languages","stargazers_url":"https://api.github.com/repos/go-gitea/test_repo/stargazers","contributors_url":"https://api.github.com/repos/go-gitea/test_repo/contributors","subscribers_url":"https://api.github.com/repos/go-gitea/test_repo/subscribers","subscription_url":"https://api.github.com/repos/go-gitea/test_repo/subscription","commits_url":"https://api.github.com/repos/go-gitea/test_repo/commits{/sha}","git_commits_url":"https://api.github.com/repos/go-gitea/test_repo/git/commits{/sha}","comments_url":"https://api.github.com/repos/go-gitea/test_repo/comments{/number}","issue_comment_url":"https://api.github.com/repos/go-gitea/test_repo/issues/comments{/number}","contents_url":"https://api.github.com/repos/go-gitea/test_repo/contents/{+path}","compare_url":"https://api.github.com/repos/go-gitea/test_repo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/go-gitea/test_repo/merges","archive_url":"https://api.github.com/repos/go-gitea/test_repo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/go-gitea/test_repo/downloads","issues_url":"https://api.github.com/repos/go-gitea/test_repo/issues{/number}","pulls_url":"https://api.github.com/repos/go-gitea/test_repo/pulls{/number}","milestones_url":"https://api.github.com/repos/go-gitea/test_repo/milestones{/number}","notifications_url":"https://api.github.com/repos/go-gitea/test_repo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/go-gitea/test_repo/labels{/name}","releases_url":"https://api.github.com/repos/go-gitea/test_repo/releases{/id}","deployments_url":"https://api.github.com/repos/go-gitea/test_repo/deployments","created_at":"2019-11-09T16:49:20Z","updated_at":"2023-03-02T14:02:26Z","pushed_at":"2019-11-12T21:54:19Z","git_url":"git://github.com/go-gitea/test_repo.git","ssh_url":"git@github.com:go-gitea/test_repo.git","clone_url":"https://github.com/go-gitea/test_repo.git","svn_url":"https://github.com/go-gitea/test_repo","homepage":"https://codeberg.org/forgejo/forgejo/","size":1,"stargazers_count":3,"watchers_count":3,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":6,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":2,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit","node_id":"MDc6TGljZW5zZTEz"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":["gitea"],"visibility":"public","forks":6,"open_issues":2,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4"},"html":{"href":"https://github.com/go-gitea/test_repo/pull/4"},"issue":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/4"},"comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/issues/4/comments"},"review_comments":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/comments"},"review_comment":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/go-gitea/test_repo/pulls/4/commits"},"statuses":{"href":"https://api.github.com/repos/go-gitea/test_repo/statuses/2be9101c543658591222acbee3eb799edfc3853d"}},"author_association":"MEMBER","auto_merge":null,"active_lock_reason":null}]
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Freleases%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Freleases%3Fpage=1&per_page=100
new file mode 100644
index 0000000000..470f5c5769
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Freleases%3Fpage=1&per_page=100
@@ -0,0 +1,24 @@
+Access-Control-Allow-Origin: *
+X-Frame-Options: deny
+Etag: W/"2986c85fcc06cc478457abb86a88ac7f065b6861e873ae0eeb9ac16a22efca45"
+X-Github-Api-Version-Selected: 2022-11-28
+X-Ratelimit-Used: 75
+X-Ratelimit-Resource: core
+Content-Type: application/json; charset=utf-8
+Cache-Control: private, max-age=60, s-maxage=60
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Accepted-Oauth-Scopes: repo
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Reset: 1730800941
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Content-Security-Policy: default-src 'none'
+X-Oauth-Scopes:
+X-Github-Media-Type: github.v3; format=json
+X-Ratelimit-Remaining: 4925
+Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
+X-Content-Type-Options: nosniff
+X-Github-Request-Id: C7CC:3118FC:3F5F41A:4035CB2:6729E6B4
+
+[{"url":"https://api.github.com/repos/go-gitea/test_repo/releases/21419432","assets_url":"https://api.github.com/repos/go-gitea/test_repo/releases/21419432/assets","upload_url":"https://uploads.github.com/repos/go-gitea/test_repo/releases/21419432/assets{?name,label}","html_url":"https://github.com/go-gitea/test_repo/releases/tag/v0.9.99","id":21419432,"author":{"login":"mrsdizzie","id":1669571,"node_id":"MDQ6VXNlcjE2Njk1NzE=","avatar_url":"https://avatars.githubusercontent.com/u/1669571?v=4","gravatar_id":"","url":"https://api.github.com/users/mrsdizzie","html_url":"https://github.com/mrsdizzie","followers_url":"https://api.github.com/users/mrsdizzie/followers","following_url":"https://api.github.com/users/mrsdizzie/following{/other_user}","gists_url":"https://api.github.com/users/mrsdizzie/gists{/gist_id}","starred_url":"https://api.github.com/users/mrsdizzie/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mrsdizzie/subscriptions","organizations_url":"https://api.github.com/users/mrsdizzie/orgs","repos_url":"https://api.github.com/users/mrsdizzie/repos","events_url":"https://api.github.com/users/mrsdizzie/events{/privacy}","received_events_url":"https://api.github.com/users/mrsdizzie/received_events","type":"User","user_view_type":"public","site_admin":false},"node_id":"MDc6UmVsZWFzZTIxNDE5NDMy","tag_name":"v0.9.99","target_commitish":"master","name":"First Release","draft":false,"prerelease":false,"created_at":"2019-11-09T16:49:21Z","published_at":"2019-11-12T20:12:10Z","assets":[],"tarball_url":"https://api.github.com/repos/go-gitea/test_repo/tarball/v0.9.99","zipball_url":"https://api.github.com/repos/go-gitea/test_repo/zipball/v0.9.99","body":"A test release"}]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/HEAD b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/HEAD
deleted file mode 100644
index b870d82622..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/HEAD
+++ /dev/null
@@ -1 +0,0 @@
-ref: refs/heads/main
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/config b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/config
deleted file mode 100644
index 07d359d07c..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/config
+++ /dev/null
@@ -1,4 +0,0 @@
-[core]
- repositoryformatversion = 0
- filemode = true
- bare = true
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/description b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/description
deleted file mode 100644
index 498b267a8c..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/description
+++ /dev/null
@@ -1 +0,0 @@
-Unnamed repository; edit this file 'description' to name the repository.
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/exclude b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/exclude
deleted file mode 100644
index a5196d1be8..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/exclude
+++ /dev/null
@@ -1,6 +0,0 @@
-# git ls-files --others --exclude-from=.git/info/exclude
-# Lines that start with '#' are comments.
-# For a project mostly in C, the following would be a good set of
-# exclude patterns (uncomment them if you want to use them):
-# *.[oa]
-# *~
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/refs b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/refs
deleted file mode 100644
index 07fe1b45cd..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/refs
+++ /dev/null
@@ -1 +0,0 @@
-ca43b48ca2c461f9a5cb66500a154b23d07c9f90 refs/heads/main
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/17/58ff42c9ab82988ad33d211ef5433afd779a9e b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/17/58ff42c9ab82988ad33d211ef5433afd779a9e
deleted file mode 100644
index 5a598008b9..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/17/58ff42c9ab82988ad33d211ef5433afd779a9e and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/1a/0f6de7bb300de4a569371cfedf1c6ff189d374 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/1a/0f6de7bb300de4a569371cfedf1c6ff189d374
deleted file mode 100644
index e296f574da..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/1a/0f6de7bb300de4a569371cfedf1c6ff189d374 and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/44/2d28a55b842472c95bead51a4c61f209ac1636 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/44/2d28a55b842472c95bead51a4c61f209ac1636
deleted file mode 100644
index f7aae32db0..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/44/2d28a55b842472c95bead51a4c61f209ac1636 and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/56/38cb8f3278e467fc1eefcac14d3c0d5d91601f b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/56/38cb8f3278e467fc1eefcac14d3c0d5d91601f
deleted file mode 100644
index b7d43b3d6d..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/56/38cb8f3278e467fc1eefcac14d3c0d5d91601f and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/59/93b8814449c4cc660132613c12cf6b26c9824c b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/59/93b8814449c4cc660132613c12cf6b26c9824c
deleted file mode 100644
index a995c75bf7..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/59/93b8814449c4cc660132613c12cf6b26c9824c and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/6d/d0c6801ddbb7333787e73e99581279492ff449 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/6d/d0c6801ddbb7333787e73e99581279492ff449
deleted file mode 100644
index aaccbfefb4..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/6d/d0c6801ddbb7333787e73e99581279492ff449 and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/76/5f7381bf10911971845cc881fa54a93389a270 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/76/5f7381bf10911971845cc881fa54a93389a270
deleted file mode 100644
index 2dfcea6cd3..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/76/5f7381bf10911971845cc881fa54a93389a270 and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7c/93d105d3ee6af50705c4d2cc8dd548c2f9edc9 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7c/93d105d3ee6af50705c4d2cc8dd548c2f9edc9
deleted file mode 100644
index 0fe680ed57..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7c/93d105d3ee6af50705c4d2cc8dd548c2f9edc9 and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7d/c3f365f3bc83011f0df5ee8637de7dd9487c04 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7d/c3f365f3bc83011f0df5ee8637de7dd9487c04
deleted file mode 100644
index 385dfc7fe7..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7d/c3f365f3bc83011f0df5ee8637de7dd9487c04
+++ /dev/null
@@ -1,3 +0,0 @@
-xUPÛŠÛ0ís¾â°O-˜´,téC)Ør,¥T’7ìc.JâÒ‚°äï;r»t†ñØç6gû+nñððå]/·¡?žÞ—pÿéþ3¶7Ôq8†ŸtM§8Œøº‹ç´Ù¥ï‡??æ<¾Íf«0<÷ãØÇ3RÄuv¬Wà9îûÏÍyÿ1Ø÷cúí5¤S?bŒ‡ô²XŽ17\®Ã%Ž/}:¿å¯ ‡ÀøS§:›s
-ûùlægj¿&+ VÖ<ÊJT¸#ÇûHWÈ ê|c,*éJE²u ¥À,KÚKá°–¾K²Œ7La±ÂºT]%õrbÉv¥$[¼a›°eÃb´Jú§É¸–^çæÚ@<
-íášlý&ÓB@IZ(š’~‚[‰R’*8®¥/˜ÿúƈÒh'~t¬ÅTÔÒ’øK}]×
ygØÑòU®S>§¯i¡ŒctÎ 6 O™Ê½qXW`ÝŽfsbâ§ôÒèŒfSoy- ÅRɥХÈD3¡½±ì¸Õ‰P€¬tÙÑt>³È‚¬¡ùœ¬8Õ;à“¿°||K“jýýóÙoQwÃÛ
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/9b/fcee0cb75322db96a76b49faa222e2c59f485d b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/9b/fcee0cb75322db96a76b49faa222e2c59f485d
deleted file mode 100644
index 64a7a811da..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/9b/fcee0cb75322db96a76b49faa222e2c59f485d and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ba/96372ae89cfd798c343542d898da029e2d8a02 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ba/96372ae89cfd798c343542d898da029e2d8a02
deleted file mode 100644
index c39c6e8a75..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ba/96372ae89cfd798c343542d898da029e2d8a02
+++ /dev/null
@@ -1,2 +0,0 @@
-x-A
-Ã0{ö+z-?ô\B> WÂQh¬b©”ü¾.ä´Ã°ùeã8]®¸[+²ñÀ,oKiYÕÑ:¢ïÇ…†øû]K£P«~ªž+Õ"ÐÎ0kàKbîe>@Ø¥0ó~¥¢(<
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/bf/2e0fd70c6ef1ab5240a340ba38eb47b3e8409f b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/bf/2e0fd70c6ef1ab5240a340ba38eb47b3e8409f
deleted file mode 100644
index e92cf50fa5..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/bf/2e0fd70c6ef1ab5240a340ba38eb47b3e8409f and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ca/43b48ca2c461f9a5cb66500a154b23d07c9f90 b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ca/43b48ca2c461f9a5cb66500a154b23d07c9f90
deleted file mode 100644
index 467ab42ca3..0000000000
Binary files a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ca/43b48ca2c461f9a5cb66500a154b23d07c9f90 and /dev/null differ
diff --git a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/refs/heads/main b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/refs/heads/main
deleted file mode 100644
index fe8dc33916..0000000000
--- a/services/migrations/testdata/github/full_download/forgejo/test_repo.git/refs/heads/main
+++ /dev/null
@@ -1 +0,0 @@
-ca43b48ca2c461f9a5cb66500a154b23d07c9f90
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce
deleted file mode 100644
index d1693ef67f..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce
+++ /dev/null
@@ -1,84 +0,0 @@
-date: Wed, 23 Jul 2025 06:51:21 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 1464
-content-security-policy: default-src 'self';script-src 'self' 'nonce-Tr8ktNGt3XoBWQBIXrrPTeA1v'; style-src 'self' 'nonce-Tr8ktNGt3XoBWQBIXrrPTeA1v'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYmI4NGRmYWMyY2Q2MzVlOWRkOTQwMGYwNzk2NzdlOGQxN2UxYTJlMCJ9.G2IX6Q.ETAG_sYgE1V9xYQNy_CD4HOP32Q; Expires=Sat, 23-Aug-2025 06:51:21 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo
deleted file mode 100644
index 063950557b..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo
+++ /dev/null
@@ -1,24 +0,0 @@
-date: Wed, 23 Jul 2025 06:44:23 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 452
-content-security-policy: default-src 'self';script-src 'self' 'nonce-jtS0HiLSSKCUK3FUuLv7XD9KG'; style-src 'self' 'nonce-jtS0HiLSSKCUK3FUuLv7XD9KG'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiY2EzN2Y5MTI1ZjkxNTkzMDViMWQ5ZGVhYjkyNzVlZTkwNmVjYzgzYiJ9.G2IWRw.1ZmhWySKFk3Lw_bA-EwmcNCgcz0; Expires=Sat, 23-Aug-2025 06:44:23 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1747635011,
- "commit_time_offset": 0,
- "committer": "Akashdeep Dhar",
- "hash": "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "message": "Change the branch identity to `test-eeee` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "1e8fa9a17b4b4ddde50f334626b0a5497070cff7"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo
deleted file mode 100644
index dbd4992dbd..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo
+++ /dev/null
@@ -1,24 +0,0 @@
-date: Wed, 23 Jul 2025 06:44:57 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 454
-content-security-policy: default-src 'self';script-src 'self' 'nonce-6gKus9S40eu36HFYePUnyxB36'; style-src 'self' 'nonce-6gKus9S40eu36HFYePUnyxB36'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZjM5ZTYyYWM5NDViYTYwMjg1MjFlMTAzNjU4OWQ3Zjc1NjA5ZmUzMSJ9.G2IWaQ._b9edyW_4DSX-umHlyVib496s00; Expires=Sat, 23-Aug-2025 06:44:57 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169509,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160",
- "message": "Change the branch identity to `test-dddd` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "586a1e8a79e572691dc086ef7bf4e2f6d34c5254"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo
deleted file mode 100644
index ce899b5e29..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo
+++ /dev/null
@@ -1,24 +0,0 @@
-date: Wed, 23 Jul 2025 06:43:47 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 452
-content-security-policy: default-src 'self';script-src 'self' 'nonce-bDXeQlGZOYsS26Mr1BJPgodzY'; style-src 'self' 'nonce-bDXeQlGZOYsS26Mr1BJPgodzY'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiNDRkYmIwMjQwODI1ODc4MGUzYWM4ZDZhZTkxYWQ2NTkyMDFhNTg0ZSJ9.G2IWIw.U2Rb6xUm4Wk9ODweB3hH1cggWkM; Expires=Sat, 23-Aug-2025 06:43:47 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1747635352,
- "commit_time_offset": 0,
- "committer": "Akashdeep Dhar",
- "hash": "1a6ccc212aa958a0fe76155c2907c889969a7224",
- "message": "Change the branch identity to `test-ffff` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "01b420e2964928a15f790f9b7c1a0053e7b5f0a5"
- ],
- "tree_id": "0c5e64a6b912cb0c3d66e66896fa98a98da69fe4"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo
deleted file mode 100644
index 412298b22c..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo
+++ /dev/null
@@ -1,24 +0,0 @@
-date: Wed, 23 Jul 2025 06:46:23 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 454
-content-security-policy: default-src 'self';script-src 'self' 'nonce-JLGc3LbKkMMzOnQTvUIIZbAwF'; style-src 'self' 'nonce-JLGc3LbKkMMzOnQTvUIIZbAwF'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYTE0Y2EzMTZiMWRkZGZjOTJjMjkwZTkyNjM5OWEzYWZiNjA0OTZiMCJ9.G2IWvw.SNBn8NelY5GBQ-8SmLY-Uwy-uq0; Expires=Sat, 23-Aug-2025 06:46:23 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169040,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "message": "Change the branch identity to `test-bbbb` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "7a23fc15f5a1463f2c425f0146def7c19ecf6c88"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo
deleted file mode 100644
index 6ac5ccf674..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo
+++ /dev/null
@@ -1,24 +0,0 @@
-date: Wed, 23 Jul 2025 06:46:59 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 454
-content-security-policy: default-src 'self';script-src 'self' 'nonce-ORfdixyFztBA0Sx54SlhQuMio'; style-src 'self' 'nonce-ORfdixyFztBA0Sx54SlhQuMio'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYmY2MjEyYzNmMDkzMmZkZDMxYjYwYzVmMDdjYzEzY2JhZTJkMjVmZCJ9.G2IW4w.JCI6ih36NlW5IPwNX1LaCbb6v5U; Expires=Sat, 23-Aug-2025 06:46:59 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169185,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "message": "Change the branch identity to `test-aaaa` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "e7911825d29e73f260de95d5070898ccbe6340a6"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo
deleted file mode 100644
index 3505297407..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo
+++ /dev/null
@@ -1,24 +0,0 @@
-date: Wed, 23 Jul 2025 06:45:31 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 454
-content-security-policy: default-src 'self';script-src 'self' 'nonce-chsmQCN0V1J9MIqK5NU6MqBJv'; style-src 'self' 'nonce-chsmQCN0V1J9MIqK5NU6MqBJv'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZDNmYWUzNTBlOWZkNDY0MTJlOWNkMjQ0M2FjZmYyY2VkZjBlY2ZiYSJ9.G2IWjA.co1poP1T_e07frtYRBAkxcp2RTM; Expires=Sat, 23-Aug-2025 06:45:32 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169354,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "f1246e331cade9341b9e4f311b7a134f99893d21",
- "message": "Change the branch identity to `test-cccc` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "f1e37736d409bb136451dfc8e3a2017d3af2ce7d"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F3 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F3
deleted file mode 100644
index a5116fa017..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F3
+++ /dev/null
@@ -1,16 +0,0 @@
-date: Wed, 23 Jul 2025 06:17:46 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 233
-content-security-policy: default-src 'self';script-src 'self' 'nonce-AyzMgRLEDXIM7Oiv1cbBRx6WC'; style-src 'self' 'nonce-AyzMgRLEDXIM7Oiv1cbBRx6WC'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiMGMxMmJkYmFmNWY1ODBiNzQxY2Q2Y2RiODk1OTRmOWMwOGY1OWM2ZSJ9.G2IQCg.BSeYpRe9-FITZnjNS1EKHjwOFmM; Expires=Sat, 23-Aug-2025 06:17:46 GMT; Secure; HttpOnly; Path=/
-content-type: text/html; charset=UTF-8
-
-
-404 Not Found
-Not Found
-The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F4 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F4
deleted file mode 100644
index e4dbc1976d..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F4
+++ /dev/null
@@ -1,16 +0,0 @@
-date: Wed, 23 Jul 2025 06:17:58 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 233
-content-security-policy: default-src 'self';script-src 'self' 'nonce-F7TmwHL5oVhCcN5Mddi8OGms7'; style-src 'self' 'nonce-F7TmwHL5oVhCcN5Mddi8OGms7'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiOTYwOWIyN2EzYjA4NmE5OTZhNjk3NWU5ZWM4OTI1MWNkN2U5OWYyMiJ9.G2IQFg.qReOtqa1f6EAhBqYOj_aQRCvgac; Expires=Sat, 23-Aug-2025 06:17:58 GMT; Secure; HttpOnly; Path=/
-content-type: text/html; charset=UTF-8
-
-
-404 Not Found
-Not Found
-The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues
deleted file mode 100644
index e04d6fd928..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues
+++ /dev/null
@@ -1,298 +0,0 @@
-date: Wed, 30 Jul 2025 05:15:59 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 18702
-content-security-policy: default-src 'self';script-src 'self' 'nonce-SA5GWMgMPH5tNiDy97SIr6jxM'; style-src 'self' 'nonce-SA5GWMgMPH5tNiDy97SIr6jxM'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiOTA1NjdkYzFjN2QyMjFhMGU3NTVmN2ZlMDYzZTgyMTQ0ZTM3NzFkNiJ9.G2s8Dw.36fFhcCc63_kPkxcJp8tz-ufC8Y; Expires=Sat, 30-Aug-2025 05:15:59 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "args": {
- "assignee": null,
- "author": null,
- "milestones": [],
- "no_stones": null,
- "order": null,
- "priority": null,
- "since": null,
- "status": "all",
- "tags": []
- },
- "issues": [
- {
- "assignee": null,
- "blocks": [],
- "close_status": "Baseless",
- "closed_at": "1750832631",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "This is the first comment under the fourth test issue",
- "date_created": "1700554045",
- "edited_on": null,
- "editor": null,
- "id": 885231,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under the fourth test issue",
- "date_created": "1700554054",
- "edited_on": null,
- "editor": null,
- "id": 885232,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue status updated to: Closed (was: Open)",
- "date_created": "1700554081",
- "edited_on": null,
- "editor": null,
- "id": 885233,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone DDDD",
- "date_created": "1746679846",
- "edited_on": null,
- "editor": null,
- "id": 971680,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: None (was: Milestone DDDD)\n- Issue status updated to: Open (was: Closed)",
- "date_created": "1750832626",
- "edited_on": null,
- "editor": null,
- "id": 976715,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue close_status updated to: Baseless\n- Issue status updated to: Closed (was: Open)",
- "date_created": "1750832632",
- "edited_on": null,
- "editor": null,
- "id": 976716,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone DDDD",
- "date_created": "1750832695",
- "edited_on": null,
- "editor": null,
- "id": 976718,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue priority set to: Common",
- "date_created": "1750832786",
- "edited_on": null,
- "editor": null,
- "id": 976722,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "content": "This is the body of the fourth test issue",
- "custom_fields": [],
- "date_created": "1700554016",
- "depends": [],
- "full_url": "https://pagure.io/protop2g-test-srce/issue/4",
- "id": 4,
- "last_updated": "1750832786",
- "milestone": "Milestone DDDD",
- "priority": 1,
- "private": true,
- "related_prs": [],
- "status": "Closed",
- "tags": [
- "gggg",
- "hhhh"
- ],
- "title": "This is the title of the fourth test issue",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": null,
- "blocks": [],
- "close_status": null,
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "This is the first comment under the third test issue",
- "date_created": "1700553880",
- "edited_on": null,
- "editor": null,
- "id": 885229,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under the third test issue",
- "date_created": "1700553892",
- "edited_on": null,
- "editor": null,
- "id": 885230,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone CCCC",
- "date_created": "1746679833",
- "edited_on": null,
- "editor": null,
- "id": 971679,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue priority set to: Uncommon",
- "date_created": "1750832767",
- "edited_on": null,
- "editor": null,
- "id": 976721,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "content": "This is the body of the third test issue",
- "custom_fields": [],
- "date_created": "1700553837",
- "depends": [],
- "full_url": "https://pagure.io/protop2g-test-srce/issue/3",
- "id": 3,
- "last_updated": "1750832767",
- "milestone": "Milestone CCCC",
- "priority": 2,
- "private": true,
- "related_prs": [],
- "status": "Open",
- "tags": [
- "eeee",
- "ffff"
- ],
- "title": "This is the title of the third test issue",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- ],
- "pagination": {
- "first": "https://pagure.io/api/0/protop2g-test-srce/issues?per_page=20&status=all&page=1",
- "last": "https://pagure.io/api/0/protop2g-test-srce/issues?per_page=20&status=all&page=1",
- "next": null,
- "page": 1,
- "pages": 1,
- "per_page": 20,
- "prev": null
- },
- "total_issues": 2
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F10 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F10
deleted file mode 100644
index 9c2bd27396..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F10
+++ /dev/null
@@ -1,258 +0,0 @@
-date: Wed, 23 Jul 2025 06:21:04 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 6179
-content-security-policy: default-src 'self';script-src 'self' 'nonce-BK18r6rq2MUNhc0CXTZKJQyJY'; style-src 'self' 'nonce-BK18r6rq2MUNhc0CXTZKJQyJY'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiOTQ3NGFiNjdkNjdlZjNiNWNmOWYwZGFmODg1MDMxMTYzNTc5OTkxMyJ9.G2IQ0A.97bsQ3k_CAN3UOID130UESba38M; Expires=Sat, 23-Aug-2025 06:21:04 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "assignee": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "branch": "main",
- "branch_from": "test-ffff",
- "cached_merge_status": "unknown",
- "closed_at": "1747635431",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: ffff\n- Request assigned",
- "commit": null,
- "date_created": "1747635211",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219623,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "rebased onto 01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "commit": null,
- "date_created": "1747635389",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219625,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been merged by t0xic0der",
- "commit": null,
- "date_created": "1747635431",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219626,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "1a6ccc212aa958a0fe76155c2907c889969a7224",
- "commit_stop": "1a6ccc212aa958a0fe76155c2907c889969a7224",
- "date_created": "1747635165",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/10",
- "id": 10,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747635431",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Merged",
- "tags": [
- "ffff"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-ffff` in the README.md file",
- "uid": "d3b7100abf8b4b02aa220d899e063295",
- "updated_on": "1747635431",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5
deleted file mode 100644
index 3fc0a4be13..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5
+++ /dev/null
@@ -1,248 +0,0 @@
-date: Wed, 23 Jul 2025 06:19:26 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 5859
-content-security-policy: default-src 'self';script-src 'self' 'nonce-x3rXi7f95hPSGE2mV9PPJv0Db'; style-src 'self' 'nonce-x3rXi7f95hPSGE2mV9PPJv0Db'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiODFiMTQzZWQwZGEzMWIxNmE3NTk1ODYyNWE0MGZjNjcwNThmMDc3ZSJ9.G2IQbg.9qdYqcrGIqnCKtjsWYjcfMCTzok; Expires=Sat, 23-Aug-2025 06:19:26 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "assignee": null,
- "branch": "main",
- "branch_from": "test-aaaa",
- "cached_merge_status": "CONFLICTS",
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: aaaa",
- "commit": null,
- "date_created": "1746427453",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219086,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under this pull request.",
- "commit": null,
- "date_created": "1746595521",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219190,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under this pull request.",
- "commit": null,
- "date_created": "1746595529",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219191,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "commit_stop": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "date_created": "1746427437",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/5",
- "id": 5,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747636185",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Open",
- "tags": [
- "aaaa"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-aaaa` in the README.md file",
- "uid": "f9e737c5ccc1434e9798cfd49d192538",
- "updated_on": "1746595529",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F6 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F6
deleted file mode 100644
index 46a27d2130..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F6
+++ /dev/null
@@ -1,248 +0,0 @@
-date: Wed, 23 Jul 2025 06:19:47 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 5859
-content-security-policy: default-src 'self';script-src 'self' 'nonce-3mH3rM1hwmaCorIQiTfUsFwdZ'; style-src 'self' 'nonce-3mH3rM1hwmaCorIQiTfUsFwdZ'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiNWY5OWMwYjViOTkzZjM0YTY3OTJiYTZmNDk4YzY3MzUwMzY3MzY2OCJ9.G2IQgw.dxyk0aB89uDJGN15BTWrZEvCFWg; Expires=Sat, 23-Aug-2025 06:19:47 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "assignee": null,
- "branch": "main",
- "branch_from": "test-bbbb",
- "cached_merge_status": "CONFLICTS",
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: bbbb",
- "commit": null,
- "date_created": "1746427480",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219087,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under this pull request.",
- "commit": null,
- "date_created": "1746595539",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219192,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under this pull request.",
- "commit": null,
- "date_created": "1746595552",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219193,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "commit_stop": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "date_created": "1746427470",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/6",
- "id": 6,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747643450",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Open",
- "tags": [
- "bbbb"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-bbbb` in the README.md file",
- "uid": "9cab89d6bb8c499e8fcb47926f1f5806",
- "updated_on": "1746595552",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F7 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F7
deleted file mode 100644
index 0693dc39f3..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F7
+++ /dev/null
@@ -1,233 +0,0 @@
-date: Wed, 23 Jul 2025 06:20:00 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 5458
-content-security-policy: default-src 'self';script-src 'self' 'nonce-Bq1C3s4EngIGuBgtUcYdmK2M8'; style-src 'self' 'nonce-Bq1C3s4EngIGuBgtUcYdmK2M8'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZjUwNDcwNWQ4Y2UxZTRjODRlNjQxYTA0NzVlNDA5NGQwNzI4YTY1NyJ9.G2IQkA.KtxZfzau2wOwMTyfG_xNBHTMHSI; Expires=Sat, 23-Aug-2025 06:20:00 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "assignee": null,
- "branch": "main",
- "branch_from": "test-cccc",
- "cached_merge_status": "FFORWARD",
- "closed_at": "1746428043",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: cccc",
- "commit": null,
- "date_created": "1746427513",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219088,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been closed by t0xic0der",
- "commit": null,
- "date_created": "1746428043",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219090,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "f1246e331cade9341b9e4f311b7a134f99893d21",
- "commit_stop": "f1246e331cade9341b9e4f311b7a134f99893d21",
- "date_created": "1746427506",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/7",
- "id": 7,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1746428043",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Closed",
- "tags": [
- "cccc"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-cccc` in the README.md file",
- "uid": "f696feab56b84557b4d4a8a4462420ee",
- "updated_on": "1746428043",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F8 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F8
deleted file mode 100644
index f5a3ad4712..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F8
+++ /dev/null
@@ -1,233 +0,0 @@
-date: Wed, 23 Jul 2025 06:20:17 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 5458
-content-security-policy: default-src 'self';script-src 'self' 'nonce-OxOLryXyFpTZ0hyi7t17U5IG0'; style-src 'self' 'nonce-OxOLryXyFpTZ0hyi7t17U5IG0'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiM2I3NWQwYzBhY2YxMjhmYTU0YmU5NzEzYzcyNjNjZTQ4NjFlNDE5ZCJ9.G2IQoQ.Ds29JFMXbO3q2kBPxmfx2kQ7YjA; Expires=Sat, 23-Aug-2025 06:20:17 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "assignee": null,
- "branch": "main",
- "branch_from": "test-dddd",
- "cached_merge_status": "FFORWARD",
- "closed_at": "1746428053",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: dddd",
- "commit": null,
- "date_created": "1746427540",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219089,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been closed by t0xic0der",
- "commit": null,
- "date_created": "1746428053",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219091,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160",
- "commit_stop": "0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160",
- "date_created": "1746427532",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/8",
- "id": 8,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1746428053",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Closed",
- "tags": [
- "dddd"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-dddd` in the README.md file",
- "uid": "493b294044fd48e18f424210c919d8de",
- "updated_on": "1746428053",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F9 b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F9
deleted file mode 100644
index f65e51e90d..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F9
+++ /dev/null
@@ -1,238 +0,0 @@
-date: Wed, 23 Jul 2025 06:20:51 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 5628
-content-security-policy: default-src 'self';script-src 'self' 'nonce-RVreTe5AdtWf0rvyk7mxX40mb'; style-src 'self' 'nonce-RVreTe5AdtWf0rvyk7mxX40mb'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiM2MyYzdlY2E0MDYzNWRmZjRhMDc2MGE0OTg2MjNhMTU4MWNhZDg4OSJ9.G2IQww.VZpiXbZftgigHkiS15g8DF6iQSY; Expires=Sat, 23-Aug-2025 06:20:51 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "assignee": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "branch": "main",
- "branch_from": "test-eeee",
- "cached_merge_status": "unknown",
- "closed_at": "1747635243",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: eeee\n- Request assigned",
- "commit": null,
- "date_created": "1747635200",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219622,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been merged by t0xic0der",
- "commit": null,
- "date_created": "1747635243",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219624,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "commit_stop": "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "date_created": "1747635161",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/9",
- "id": 9,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747635243",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Merged",
- "tags": [
- "eeee"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-eeee` in the README.md file",
- "uid": "f2ad806e430a40bd8ee5894484338df4",
- "updated_on": "1747635243",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests
deleted file mode 100644
index a047968f02..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests
+++ /dev/null
@@ -1,506 +0,0 @@
-date: Wed, 23 Jul 2025 06:18:47 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 14090
-content-security-policy: default-src 'self';script-src 'self' 'nonce-t1H8BCX7kXOBmXS0wHPpBrAhK'; style-src 'self' 'nonce-t1H8BCX7kXOBmXS0wHPpBrAhK'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiODAwYWYyZGI3MjZlMzA2ZTdmNTdlMmIwNGVkNmU3YTBmNDYwZDUyMyJ9.G2IQRw.EDXBH36zsKcHKDETH_g7miO_r_w; Expires=Sat, 23-Aug-2025 06:18:47 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "args": {
- "assignee": null,
- "author": null,
- "page": 1,
- "per_page": 20,
- "status": true,
- "tags": []
- },
- "pagination": {
- "first": "https://pagure.io/api/0/protop2g-test-srce/pull-requests?per_page=20&page=1",
- "last": "https://pagure.io/api/0/protop2g-test-srce/pull-requests?per_page=20&page=1",
- "next": null,
- "page": 1,
- "pages": 1,
- "per_page": 20,
- "prev": null
- },
- "requests": [
- {
- "assignee": null,
- "branch": "main",
- "branch_from": "test-bbbb",
- "cached_merge_status": "CONFLICTS",
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: bbbb",
- "commit": null,
- "date_created": "1746427480",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219087,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under this pull request.",
- "commit": null,
- "date_created": "1746595539",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219192,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under this pull request.",
- "commit": null,
- "date_created": "1746595552",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219193,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "commit_stop": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "date_created": "1746427470",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/6",
- "id": 6,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747643450",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Open",
- "tags": [
- "bbbb"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-bbbb` in the README.md file",
- "uid": "9cab89d6bb8c499e8fcb47926f1f5806",
- "updated_on": "1746595552",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": null,
- "branch": "main",
- "branch_from": "test-aaaa",
- "cached_merge_status": "CONFLICTS",
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: aaaa",
- "commit": null,
- "date_created": "1746427453",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219086,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under this pull request.",
- "commit": null,
- "date_created": "1746595521",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219190,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under this pull request.",
- "commit": null,
- "date_created": "1746595529",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219191,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "commit_stop": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "date_created": "1746427437",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/5",
- "id": 5,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747636185",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Open",
- "tags": [
- "aaaa"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-aaaa` in the README.md file",
- "uid": "f9e737c5ccc1434e9798cfd49d192538",
- "updated_on": "1746595529",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "total_requests": 2
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa
deleted file mode 100644
index 5afe87654e..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa
+++ /dev/null
@@ -1,17 +0,0 @@
-date: Wed, 23 Jul 2025 05:34:39 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 77
-content-security-policy: default-src 'self';script-src 'self' 'nonce-gI8Srx27RgY20Zqw5vVkWVORV'; style-src 'self' 'nonce-gI8Srx27RgY20Zqw5vVkWVORV'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZmJhYTlmMTAzYzYxMWE5NmZiZDNkZjE1ZmRiZGQzYTA5YTQ1YTM2YyJ9.G2IF7w.CLP1edDfZSoJcXjoUNt40swcBV8; Expires=Sat, 23-Aug-2025 05:34:39 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tag": "aaaa",
- "tag_color": "#ff0000",
- "tag_description": "aaaa"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb
deleted file mode 100644
index 37d40b9c6e..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb
+++ /dev/null
@@ -1,17 +0,0 @@
-date: Wed, 23 Jul 2025 05:34:50 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 77
-content-security-policy: default-src 'self';script-src 'self' 'nonce-wgQpgRvwBbyoY9WWTv55wb8Dd'; style-src 'self' 'nonce-wgQpgRvwBbyoY9WWTv55wb8Dd'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZWU4ZDUxYzY4NmUzMzM4ODg4YmRmMjQwYmU3ODVhOTA4MzQ4N2Q2NiJ9.G2IF-g.nG1k1zU4b9Eo9WFGCas8R9a5-Vg; Expires=Sat, 23-Aug-2025 05:34:50 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tag": "bbbb",
- "tag_color": "#ff0000",
- "tag_description": "bbbb"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc
deleted file mode 100644
index 4f170a50fe..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc
+++ /dev/null
@@ -1,17 +0,0 @@
-date: Wed, 23 Jul 2025 05:35:02 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 77
-content-security-policy: default-src 'self';script-src 'self' 'nonce-kAxTKES1vWlzLCIScejGQ5JpX'; style-src 'self' 'nonce-kAxTKES1vWlzLCIScejGQ5JpX'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYjhhM2YxNjJhZDYwYWU5NzE4NmQ4NDBkMzJlOWNkYmYxN2Y2NjBmMyJ9.G2IGBg.oA7d9DJHW4_ilpbiVkbveF5dM3Q; Expires=Sat, 23-Aug-2025 05:35:02 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tag": "cccc",
- "tag_color": "#ffff00",
- "tag_description": "cccc"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd
deleted file mode 100644
index fb79ce7a97..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd
+++ /dev/null
@@ -1,17 +0,0 @@
-date: Wed, 23 Jul 2025 05:36:23 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 77
-content-security-policy: default-src 'self';script-src 'self' 'nonce-rzXQsxeaBjeye4rVcEn3aSWKa'; style-src 'self' 'nonce-rzXQsxeaBjeye4rVcEn3aSWKa'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiY2VhZjRhOTlkZWUxZDcxODg2NWIyN2JhZGY4ZjUyMjcxZTdkZGU0MyJ9.G2IGVw.JCK3tXfD0aOgDdIAMv5MlFkl-SY; Expires=Sat, 23-Aug-2025 05:36:23 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tag": "dddd",
- "tag_color": "#ffff00",
- "tag_description": "dddd"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee
deleted file mode 100644
index 5e968f23b9..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee
+++ /dev/null
@@ -1,17 +0,0 @@
-date: Wed, 23 Jul 2025 05:36:34 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 77
-content-security-policy: default-src 'self';script-src 'self' 'nonce-gs07ooad3wPkkzZKAQDHDTrMl'; style-src 'self' 'nonce-gs07ooad3wPkkzZKAQDHDTrMl'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiOGEyNDUwYzk5MDY2ZjJhZGQyNDhlZmZmM2QxN2UxZTM0ODI2NWFhZiJ9.G2IGYg.hM6ZKEPDXtOvTWlSPeQBLiZjCO4; Expires=Sat, 23-Aug-2025 05:36:34 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tag": "eeee",
- "tag_color": "#00ff00",
- "tag_description": "eeee"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff
deleted file mode 100644
index 83d48ee851..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff
+++ /dev/null
@@ -1,17 +0,0 @@
-date: Wed, 23 Jul 2025 05:36:48 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 77
-content-security-policy: default-src 'self';script-src 'self' 'nonce-hiIOP1ZdLfgxJERlzriEOATjs'; style-src 'self' 'nonce-hiIOP1ZdLfgxJERlzriEOATjs'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYWMyNjBhNjE5MjI0OTQ0YTU2Yzc5YjNmNzU3ZTU2MTYzZGQwMGMwNSJ9.G2IGcQ.22KbaZBjPxJkpIoTBIn1UtVzEjI; Expires=Sat, 23-Aug-2025 05:36:49 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tag": "ffff",
- "tag_color": "#00ff00",
- "tag_description": "ffff"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg
deleted file mode 100644
index 85a07333f8..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg
+++ /dev/null
@@ -1,17 +0,0 @@
-date: Wed, 23 Jul 2025 05:37:23 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 77
-content-security-policy: default-src 'self';script-src 'self' 'nonce-FYnaELqaJomAM4RnLtKUr7gbE'; style-src 'self' 'nonce-FYnaELqaJomAM4RnLtKUr7gbE'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiOGExMmI2MGZhYTQyYTY5MmQ0MzNkNWFlZTU0YjE4M2NjY2NmYjI3MCJ9.G2IGkw.9-wL70lPAOlpIv8cusGLLA0Np_U; Expires=Sat, 23-Aug-2025 05:37:23 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tag": "gggg",
- "tag_color": "#0000ff",
- "tag_description": "gggg"
-}
diff --git a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags
deleted file mode 100644
index 131f4b34a8..0000000000
--- a/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags
+++ /dev/null
@@ -1,25 +0,0 @@
-date: Wed, 23 Jul 2025 05:16:48 GMT
-server: Apache/2.4.37 (Red Hat Enterprise Linux) OpenSSL/1.1.1k mod_wsgi/4.6.4 Python/3.6
-x-xss-protection: 1; mode=block
-x-content-type-options: nosniff
-referrer-policy: same-origin
-x-frame-options: ALLOW-FROM https://pagure.io/
-strict-transport-security: max-age=31536000; includeSubDomains; preload
-content-length: 142
-content-security-policy: default-src 'self';script-src 'self' 'nonce-T8LPhGGs1acv00t58vtBtVmwC'; style-src 'self' 'nonce-T8LPhGGs1acv00t58vtBtVmwC'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-set-cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYmQ2NTQ1MmFhZWJjZjA0OWIxNTI5MjBjODQyYzUzOGRkYmYyYTkwOSJ9.G2IBwA.pKqOBVDJrtKfyrbJPyuaRfFjnR4; Expires=Sat, 23-Aug-2025 05:16:48 GMT; Secure; HttpOnly; Path=/
-content-type: application/json
-
-{
- "tags": [
- "aaaa",
- "bbbb",
- "cccc",
- "dddd",
- "eeee",
- "ffff",
- "gggg",
- "hhhh"
- ],
- "total_tags": 8
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce
deleted file mode 100644
index ef69d4bdb8..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce
+++ /dev/null
@@ -1,82 +0,0 @@
-Content-Length: 1464
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-tnXvEZGNmF5HI1BOPEvAXnIFi'; style-src 'self' 'nonce-tnXvEZGNmF5HI1BOPEvAXnIFi'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiOGEzYjk3NGI3NWM1MjdjYzY5MjdhM2E5MjZhNjZiZTBlNWFmYjM5MCJ9.G4-gsg.8A24PiXaZg1Cu-jRIk80lUXLWFo; Expires=Fri, 26-Sep-2025 19:46:58 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-
-{
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo
deleted file mode 100644
index 2501f4f6e3..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo
+++ /dev/null
@@ -1,22 +0,0 @@
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-X-Xss-Protection: 1; mode=block
-Referrer-Policy: same-origin
-Content-Length: 452
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-Y3ZN3d6QZqSLwh8xAkWYBzeiT'; style-src 'self' 'nonce-Y3ZN3d6QZqSLwh8xAkWYBzeiT'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiMzJiMzRmYTllNjVkMTQzYjQzNGIxOTlmNzliODZlOTRjMzA3NmQ1MiJ9.G4-gtg.PdKDFShx8RPQiN9BHaw7eAf_uzI; Expires=Fri, 26-Sep-2025 19:47:02 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Content-Type-Options: nosniff
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1747635011,
- "commit_time_offset": 0,
- "committer": "Akashdeep Dhar",
- "hash": "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "message": "Change the branch identity to `test-eeee` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "1e8fa9a17b4b4ddde50f334626b0a5497070cff7"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo
deleted file mode 100644
index 89e6c4cae6..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo
+++ /dev/null
@@ -1,22 +0,0 @@
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiNGU2MzY4YmE0MGE3NDdhYWVlZDUwNDczOTlhMWZiZGY5YjhjNWJjMSJ9.G4-gtg.nsP6pJHATDf8xW73FOOSlwhGUkE; Expires=Fri, 26-Sep-2025 19:47:02 GMT; Secure; HttpOnly; Path=/
-Content-Length: 454
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-LfLHOABekbvGWQH4XCgWCCMsc'; style-src 'self' 'nonce-LfLHOABekbvGWQH4XCgWCCMsc'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169509,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160",
- "message": "Change the branch identity to `test-dddd` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "586a1e8a79e572691dc086ef7bf4e2f6d34c5254"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo
deleted file mode 100644
index 5fc6687589..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo
+++ /dev/null
@@ -1,22 +0,0 @@
-X-Xss-Protection: 1; mode=block
-Content-Length: 452
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZDAxNjdjNGQwYTI4ZDRiY2RlNDVmN2NmMjE4YmExOTEyZDQyMTNhMCJ9.G4-gtQ.pOTn9bI3Kb4T50pd_-8wV840Reg; Expires=Fri, 26-Sep-2025 19:47:01 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-ye8eyhQR7cRy062VVRWLSeYzI'; style-src 'self' 'nonce-ye8eyhQR7cRy062VVRWLSeYzI'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1747635352,
- "commit_time_offset": 0,
- "committer": "Akashdeep Dhar",
- "hash": "1a6ccc212aa958a0fe76155c2907c889969a7224",
- "message": "Change the branch identity to `test-ffff` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "01b420e2964928a15f790f9b7c1a0053e7b5f0a5"
- ],
- "tree_id": "0c5e64a6b912cb0c3d66e66896fa98a98da69fe4"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo
deleted file mode 100644
index 637f9b44da..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo
+++ /dev/null
@@ -1,22 +0,0 @@
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-z4zIwQCgXhVVY9lil5w37aAVg'; style-src 'self' 'nonce-z4zIwQCgXhVVY9lil5w37aAVg'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-X-Content-Type-Options: nosniff
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Length: 454
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiNzRiY2Q2NzUzYmMyMDgyZmNmYTQ2NGQ0N2Y5ODhmNjhiMzYxMjMwMyJ9.G4-gtg.mUZvuxqGocR65nDhnE43Iwl7qtQ; Expires=Fri, 26-Sep-2025 19:47:02 GMT; Secure; HttpOnly; Path=/
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169040,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "message": "Change the branch identity to `test-bbbb` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "7a23fc15f5a1463f2c425f0146def7c19ecf6c88"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo
deleted file mode 100644
index 5f04c62717..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo
+++ /dev/null
@@ -1,22 +0,0 @@
-X-Content-Type-Options: nosniff
-Content-Length: 454
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZGYyYjkwMjU1YWI2MTU1MGMzN2IwNDk5NDMyNmQ5MDhjOTQwNDM1MCJ9.G4-gtg.lauXHyPuU-GpljnUX7l5muhxnQ4; Expires=Fri, 26-Sep-2025 19:47:02 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-gTkLO2hyzMbZ7VoCsariFBw5p'; style-src 'self' 'nonce-gTkLO2hyzMbZ7VoCsariFBw5p'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169185,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "message": "Change the branch identity to `test-aaaa` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "e7911825d29e73f260de95d5070898ccbe6340a6"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo
deleted file mode 100644
index a0f1ee088e..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo
+++ /dev/null
@@ -1,22 +0,0 @@
-Content-Length: 454
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYWQyOWJlMDZjOTE5OTdlMGJjZDY5Y2U2NWZiZDQ1N2I0YzFjNzcyYiJ9.G4-gtg.AcC4LKuD5ETKZhTYsrK7sIMQTkU; Expires=Fri, 26-Sep-2025 19:47:02 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-7wtiizCOJwkK1brnaNWuEQwLc'; style-src 'self' 'nonce-7wtiizCOJwkK1brnaNWuEQwLc'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-
-{
- "author": "Akashdeep Dhar",
- "commit_time": 1697169354,
- "commit_time_offset": 330,
- "committer": "Akashdeep Dhar",
- "hash": "f1246e331cade9341b9e4f311b7a134f99893d21",
- "message": "Change the branch identity to `test-cccc` in the README.md file\n\nSigned-off-by: Akashdeep Dhar \n",
- "parent_ids": [
- "3f12d300f62f1c5b8a1d3265bd85d61cf6d924d7"
- ],
- "tree_id": "f1e37736d409bb136451dfc8e3a2017d3af2ce7d"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F2 b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F2
deleted file mode 100644
index 34547248b4..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F2
+++ /dev/null
@@ -1,191 +0,0 @@
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Length: 5327
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-ZantBGThWBCI2APLt4SMU7xGo'; style-src 'self' 'nonce-ZantBGThWBCI2APLt4SMU7xGo'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiN2MyZWQyMWNlNTAyMDZhYTNlNjY2MThhZWE1ODJhM2Q0YjNlYWJiOCJ9.G4-gtQ.qChOO4poXg469ntQDFbUfu7Cdoo; Expires=Fri, 26-Sep-2025 19:47:01 GMT; Secure; HttpOnly; Path=/
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Type: application/json
-
-{
- "assignee": null,
- "blocks": [],
- "close_status": "Complete",
- "closed_at": "1750832579",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue tagged with: cccc, dddd",
- "date_created": "1697169810",
- "edited_on": null,
- "editor": null,
- "id": 878472,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "The is the first comment under the second test issue",
- "date_created": "1697169964",
- "edited_on": null,
- "editor": null,
- "id": 878475,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "The is the second comment under the second test issue",
- "date_created": "1697169976",
- "edited_on": null,
- "editor": null,
- "id": 878476,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue status updated to: Closed (was: Open)",
- "date_created": "1697170032",
- "edited_on": null,
- "editor": null,
- "id": 878477,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone BBBB",
- "date_created": "1746679821",
- "edited_on": null,
- "editor": null,
- "id": 971678,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: None (was: Milestone BBBB)\n- Issue status updated to: Open (was: Closed)",
- "date_created": "1750832572",
- "edited_on": null,
- "editor": null,
- "id": 976713,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue close_status updated to: Complete\n- Issue status updated to: Closed (was: Open)",
- "date_created": "1750832582",
- "edited_on": null,
- "editor": null,
- "id": 976714,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone BBBB",
- "date_created": "1750832674",
- "edited_on": null,
- "editor": null,
- "id": 976717,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue priority set to: Rare",
- "date_created": "1750832757",
- "edited_on": null,
- "editor": null,
- "id": 976720,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "content": "This is the body of the second test issue",
- "custom_fields": [],
- "date_created": "1697169676",
- "depends": [],
- "full_url": "https://pagure.io/protop2g-test-srce/issue/2",
- "id": 2,
- "last_updated": "1750832757",
- "milestone": "Milestone BBBB",
- "priority": 3,
- "private": false,
- "related_prs": [],
- "status": "Closed",
- "tags": [
- "cccc",
- "dddd"
- ],
- "title": "This is the title of the second test issue",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues%3Fpage=1&per_page=20&status=all b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues%3Fpage=1&per_page=20&status=all
deleted file mode 100644
index 683954bdfa..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues%3Fpage=1&per_page=20&status=all
+++ /dev/null
@@ -1,328 +0,0 @@
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Length: 10166
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-PksumVGOMvBkmvCTEaSHSrExE'; style-src 'self' 'nonce-PksumVGOMvBkmvCTEaSHSrExE'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZDRiMzUyNTA1NDU0MDU4ZTJhNjA5NWFkZjNkYjRkMmUwYjYzZGZkNCJ9.G4-gtA.hMuX3dKcBI5jKIZp5DlZWJhSDPw; Expires=Fri, 26-Sep-2025 19:47:00 GMT; Secure; HttpOnly; Path=/
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-
-{
- "args": {
- "assignee": null,
- "author": null,
- "milestones": [],
- "no_stones": null,
- "order": null,
- "priority": null,
- "since": null,
- "status": "all",
- "tags": []
- },
- "issues": [
- {
- "assignee": null,
- "blocks": [],
- "close_status": "Complete",
- "closed_at": "1750832579",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue tagged with: cccc, dddd",
- "date_created": "1697169810",
- "edited_on": null,
- "editor": null,
- "id": 878472,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "The is the first comment under the second test issue",
- "date_created": "1697169964",
- "edited_on": null,
- "editor": null,
- "id": 878475,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "The is the second comment under the second test issue",
- "date_created": "1697169976",
- "edited_on": null,
- "editor": null,
- "id": 878476,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue status updated to: Closed (was: Open)",
- "date_created": "1697170032",
- "edited_on": null,
- "editor": null,
- "id": 878477,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone BBBB",
- "date_created": "1746679821",
- "edited_on": null,
- "editor": null,
- "id": 971678,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: None (was: Milestone BBBB)\n- Issue status updated to: Open (was: Closed)",
- "date_created": "1750832572",
- "edited_on": null,
- "editor": null,
- "id": 976713,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue close_status updated to: Complete\n- Issue status updated to: Closed (was: Open)",
- "date_created": "1750832582",
- "edited_on": null,
- "editor": null,
- "id": 976714,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone BBBB",
- "date_created": "1750832674",
- "edited_on": null,
- "editor": null,
- "id": 976717,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue priority set to: Rare",
- "date_created": "1750832757",
- "edited_on": null,
- "editor": null,
- "id": 976720,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "content": "This is the body of the second test issue",
- "custom_fields": [],
- "date_created": "1697169676",
- "depends": [],
- "full_url": "https://pagure.io/protop2g-test-srce/issue/2",
- "id": 2,
- "last_updated": "1750832757",
- "milestone": "Milestone BBBB",
- "priority": 3,
- "private": false,
- "related_prs": [],
- "status": "Closed",
- "tags": [
- "cccc",
- "dddd"
- ],
- "title": "This is the title of the second test issue",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": null,
- "blocks": [],
- "close_status": null,
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue tagged with: aaaa, bbbb",
- "date_created": "1697169699",
- "edited_on": null,
- "editor": null,
- "id": 878471,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under the first test issue",
- "date_created": "1697169880",
- "edited_on": null,
- "editor": null,
- "id": 878473,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under the first test issue",
- "date_created": "1697169924",
- "edited_on": null,
- "editor": null,
- "id": 878474,
- "notification": false,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue set to the milestone: Milestone AAAA",
- "date_created": "1746679806",
- "edited_on": null,
- "editor": null,
- "id": 971677,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Issue priority set to: Epic",
- "date_created": "1750832745",
- "edited_on": null,
- "editor": null,
- "id": 976719,
- "notification": true,
- "parent": null,
- "reactions": {},
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "content": "This is the body of the first test issue",
- "custom_fields": [],
- "date_created": "1697169462",
- "depends": [],
- "full_url": "https://pagure.io/protop2g-test-srce/issue/1",
- "id": 1,
- "last_updated": "1750832745",
- "milestone": "Milestone AAAA",
- "priority": 4,
- "private": false,
- "related_prs": [],
- "status": "Open",
- "tags": [
- "aaaa",
- "bbbb"
- ],
- "title": "This is the title of the first test issue",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "pagination": {
- "first": "https://pagure.io/api/0/protop2g-test-srce/issues?per_page=20&status=all&page=1",
- "last": "https://pagure.io/api/0/protop2g-test-srce/issues?per_page=20&status=all&page=1",
- "next": null,
- "page": 1,
- "pages": 1,
- "per_page": 20,
- "prev": null
- },
- "total_issues": 2
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5 b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5
deleted file mode 100644
index 18475372de..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5
+++ /dev/null
@@ -1,246 +0,0 @@
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Length: 5859
-Referrer-Policy: same-origin
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-cKJRnMfn3wZ2XOdNVb5fJc3EU'; style-src 'self' 'nonce-cKJRnMfn3wZ2XOdNVb5fJc3EU'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiNDI2OTlmMzdkNzliNDBhZTNjNWVjMzI0ZjJiZTZhZTRiOTllMDc1YiJ9.G4-gtw.IeZ5E3NAmb0f8u4EtYgCVD8wupw; Expires=Fri, 26-Sep-2025 19:47:03 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-
-{
- "assignee": null,
- "branch": "main",
- "branch_from": "test-aaaa",
- "cached_merge_status": "CONFLICTS",
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: aaaa",
- "commit": null,
- "date_created": "1746427453",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219086,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under this pull request.",
- "commit": null,
- "date_created": "1746595521",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219190,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under this pull request.",
- "commit": null,
- "date_created": "1746595529",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219191,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "commit_stop": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "date_created": "1746427437",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/5",
- "id": 5,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747636185",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Open",
- "tags": [
- "aaaa"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-aaaa` in the README.md file",
- "uid": "f9e737c5ccc1434e9798cfd49d192538",
- "updated_on": "1746595529",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests%3Fpage=1&per_page=20&status=all b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests%3Fpage=1&per_page=20&status=all
deleted file mode 100644
index f9127f0899..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests%3Fpage=1&per_page=20&status=all
+++ /dev/null
@@ -1,1418 +0,0 @@
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiMGIxOTQwMmQ5ZGRhMjk4NDZhM2VkNTFhMWI0MzZjNWQ4MTZmNTczYSJ9.G4-gtQ.NMS0gvCElG-a6Nm6IwwBJi3TSUM; Expires=Fri, 26-Sep-2025 19:47:01 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Referrer-Policy: same-origin
-Content-Length: 40500
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-wa5O4laHUSWMEwIK4wwBWPPTY'; style-src 'self' 'nonce-wa5O4laHUSWMEwIK4wwBWPPTY'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-
-{
- "args": {
- "assignee": null,
- "author": null,
- "page": 1,
- "per_page": 20,
- "status": "all",
- "tags": []
- },
- "pagination": {
- "first": "https://pagure.io/api/0/protop2g-test-srce/pull-requests?per_page=20&status=all&page=1",
- "last": "https://pagure.io/api/0/protop2g-test-srce/pull-requests?per_page=20&status=all&page=1",
- "next": null,
- "page": 1,
- "pages": 1,
- "per_page": 20,
- "prev": null
- },
- "requests": [
- {
- "assignee": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "branch": "main",
- "branch_from": "test-ffff",
- "cached_merge_status": "unknown",
- "closed_at": "1747635431",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: ffff\n- Request assigned",
- "commit": null,
- "date_created": "1747635211",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219623,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "rebased onto 01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "commit": null,
- "date_created": "1747635389",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219625,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been merged by t0xic0der",
- "commit": null,
- "date_created": "1747635431",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219626,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "1a6ccc212aa958a0fe76155c2907c889969a7224",
- "commit_stop": "1a6ccc212aa958a0fe76155c2907c889969a7224",
- "date_created": "1747635165",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/10",
- "id": 10,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747635431",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Merged",
- "tags": [
- "ffff"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-ffff` in the README.md file",
- "uid": "d3b7100abf8b4b02aa220d899e063295",
- "updated_on": "1747635431",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "branch": "main",
- "branch_from": "test-eeee",
- "cached_merge_status": "unknown",
- "closed_at": "1747635243",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: eeee\n- Request assigned",
- "commit": null,
- "date_created": "1747635200",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219622,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been merged by t0xic0der",
- "commit": null,
- "date_created": "1747635243",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219624,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "commit_stop": "01b420e2964928a15f790f9b7c1a0053e7b5f0a5",
- "date_created": "1747635161",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/9",
- "id": 9,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747635243",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Merged",
- "tags": [
- "eeee"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-eeee` in the README.md file",
- "uid": "f2ad806e430a40bd8ee5894484338df4",
- "updated_on": "1747635243",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": null,
- "branch": "main",
- "branch_from": "test-dddd",
- "cached_merge_status": "FFORWARD",
- "closed_at": "1746428053",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: dddd",
- "commit": null,
- "date_created": "1746427540",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219089,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been closed by t0xic0der",
- "commit": null,
- "date_created": "1746428053",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219091,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160",
- "commit_stop": "0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160",
- "date_created": "1746427532",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/8",
- "id": 8,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1746428053",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Closed",
- "tags": [
- "dddd"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-dddd` in the README.md file",
- "uid": "493b294044fd48e18f424210c919d8de",
- "updated_on": "1746428053",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": null,
- "branch": "main",
- "branch_from": "test-cccc",
- "cached_merge_status": "FFORWARD",
- "closed_at": "1746428043",
- "closed_by": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- },
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: cccc",
- "commit": null,
- "date_created": "1746427513",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219088,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "Pull-Request has been closed by t0xic0der",
- "commit": null,
- "date_created": "1746428043",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219090,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "f1246e331cade9341b9e4f311b7a134f99893d21",
- "commit_stop": "f1246e331cade9341b9e4f311b7a134f99893d21",
- "date_created": "1746427506",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/7",
- "id": 7,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1746428043",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Closed",
- "tags": [
- "cccc"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-cccc` in the README.md file",
- "uid": "f696feab56b84557b4d4a8a4462420ee",
- "updated_on": "1746428043",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": null,
- "branch": "main",
- "branch_from": "test-bbbb",
- "cached_merge_status": "CONFLICTS",
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: bbbb",
- "commit": null,
- "date_created": "1746427480",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219087,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under this pull request.",
- "commit": null,
- "date_created": "1746595539",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219192,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under this pull request.",
- "commit": null,
- "date_created": "1746595552",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219193,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "commit_stop": "2d40761dc53e6fa060ac49d88e1452c6751d4b1c",
- "date_created": "1746427470",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/6",
- "id": 6,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747643450",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Open",
- "tags": [
- "bbbb"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-bbbb` in the README.md file",
- "uid": "9cab89d6bb8c499e8fcb47926f1f5806",
- "updated_on": "1746595552",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "assignee": null,
- "branch": "main",
- "branch_from": "test-aaaa",
- "cached_merge_status": "CONFLICTS",
- "closed_at": null,
- "closed_by": null,
- "comments": [
- {
- "comment": "**Metadata Update from @t0xic0der**:\n- Pull-request tagged with: aaaa",
- "commit": null,
- "date_created": "1746427453",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219086,
- "line": null,
- "notification": true,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the first comment under this pull request.",
- "commit": null,
- "date_created": "1746595521",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219190,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- {
- "comment": "This is the second comment under this pull request.",
- "commit": null,
- "date_created": "1746595529",
- "edited_on": null,
- "editor": null,
- "filename": null,
- "id": 219191,
- "line": null,
- "notification": false,
- "parent": null,
- "reactions": {},
- "tree": null,
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "commit_start": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "commit_stop": "b55e5c91d2572d60a8d7e71b3d3003e523127bd4",
- "date_created": "1746427437",
- "full_url": "https://pagure.io/protop2g-test-srce/pull-request/5",
- "id": 5,
- "initial_comment": "Signed-off-by: Akashdeep Dhar ",
- "last_updated": "1747636185",
- "project": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "remote_git": null,
- "repo_from": {
- "access_groups": {
- "admin": [],
- "collaborator": [],
- "commit": [],
- "ticket": []
- },
- "access_users": {
- "admin": [
- "ryanlerch"
- ],
- "collaborator": [],
- "commit": [],
- "owner": [
- "t0xic0der"
- ],
- "ticket": []
- },
- "close_status": [
- "Complete",
- "Baseless"
- ],
- "custom_keys": [],
- "date_created": "1697168063",
- "date_modified": "1744795940",
- "description": "The source namespace for the Pagure Exporter project to run tests against",
- "full_url": "https://pagure.io/protop2g-test-srce",
- "fullname": "protop2g-test-srce",
- "id": 17042,
- "milestones": {
- "Milestone AAAA": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone BBBB": {
- "active": false,
- "date": "1765497600"
- },
- "Milestone CCCC": {
- "active": true,
- "date": "1765497600"
- },
- "Milestone DDDD": {
- "active": false,
- "date": "1765497600"
- }
- },
- "name": "protop2g-test-srce",
- "namespace": null,
- "parent": null,
- "priorities": {
- "": "",
- "1": "Common",
- "2": "Uncommon",
- "3": "Rare",
- "4": "Epic",
- "5": "Mythic"
- },
- "tags": [
- "srce",
- "test",
- "gridhead",
- "protop2g"
- ],
- "url_path": "protop2g-test-srce",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- },
- "status": "Open",
- "tags": [
- "aaaa"
- ],
- "threshold_reached": null,
- "title": "Change the branch identity to `test-aaaa` in the README.md file",
- "uid": "f9e737c5ccc1434e9798cfd49d192538",
- "updated_on": "1746595529",
- "user": {
- "full_url": "https://pagure.io/user/t0xic0der",
- "fullname": "Akashdeep Dhar",
- "name": "t0xic0der",
- "url_path": "user/t0xic0der"
- }
- }
- ],
- "total_requests": 6
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa
deleted file mode 100644
index 416eb8be84..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa
+++ /dev/null
@@ -1,15 +0,0 @@
-X-Xss-Protection: 1; mode=block
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Length: 77
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-NY1utFEeOZi2ktclBSK9yPBbQ'; style-src 'self' 'nonce-NY1utFEeOZi2ktclBSK9yPBbQ'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiNDUzNjY2Zjg4OGI5YThiNjU4N2Y3YzU1ZTk4ZTEwZGZhMGZiM2Q1MSJ9.G4-gsw.MW8WhWH9mQd10iDTu49EExAjtOU; Expires=Fri, 26-Sep-2025 19:46:59 GMT; Secure; HttpOnly; Path=/
-X-Content-Type-Options: nosniff
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Type: application/json
-
-{
- "tag": "aaaa",
- "tag_color": "#ff0000",
- "tag_description": "aaaa"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb
deleted file mode 100644
index b0bea5052f..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb
+++ /dev/null
@@ -1,15 +0,0 @@
-X-Xss-Protection: 1; mode=block
-Referrer-Policy: same-origin
-Content-Length: 77
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-3WQfyrKHlOVMS6hbVxdWffV0z'; style-src 'self' 'nonce-3WQfyrKHlOVMS6hbVxdWffV0z'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYzM2NjVjMjRkYjU5Nzk2NzBiZjRlMmY4NWNmYTIwMGU3NWFjNTg2MiJ9.G4-gsw.wmhIp0DsAhRYagA9qZFYM5jWWt0; Expires=Fri, 26-Sep-2025 19:46:59 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Content-Type-Options: nosniff
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-
-{
- "tag": "bbbb",
- "tag_color": "#ff0000",
- "tag_description": "bbbb"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc
deleted file mode 100644
index 378d374b36..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc
+++ /dev/null
@@ -1,15 +0,0 @@
-Content-Length: 77
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-n8CH3n7QkxYS4ITojGCPv0Qaf'; style-src 'self' 'nonce-n8CH3n7QkxYS4ITojGCPv0Qaf'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiMDQyNDhlM2I3NDMzNWU3MDNkOWQ4MDk5M2FhNmYzZTk1N2M3NWFiNiJ9.G4-gsw.fIL0G11hmPPmHfKgqa3UGbhc3Wg; Expires=Fri, 26-Sep-2025 19:46:59 GMT; Secure; HttpOnly; Path=/
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-
-{
- "tag": "cccc",
- "tag_color": "#ffff00",
- "tag_description": "cccc"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd
deleted file mode 100644
index fa6d75ff1d..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd
+++ /dev/null
@@ -1,15 +0,0 @@
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Length: 77
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-Paip3h7855Wqz66JatzO28HOo'; style-src 'self' 'nonce-Paip3h7855Wqz66JatzO28HOo'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYjIxMzk4ZjQwMDM3NjdmMWY0MjI5YTExYTVlNDQwODlhYWJhNTgyOSJ9.G4-gsw.cpd52huyCk-zDHjOrMBhHmL5STo; Expires=Fri, 26-Sep-2025 19:46:59 GMT; Secure; HttpOnly; Path=/
-Referrer-Policy: same-origin
-
-{
- "tag": "dddd",
- "tag_color": "#ffff00",
- "tag_description": "dddd"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee
deleted file mode 100644
index 2f4045095b..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee
+++ /dev/null
@@ -1,15 +0,0 @@
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Length: 77
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-IeZeJPDSNyxAmSQ2Pe27r2Pkh'; style-src 'self' 'nonce-IeZeJPDSNyxAmSQ2Pe27r2Pkh'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiNWQ1NzQ3Yzg5ZDlkZGI2ZTVhNzUyYTk4YzAyMGFiYjg2Mzg1ZWFiYiJ9.G4-gsw.4o-K9_mwm_kGFNG-SVoQTu-nCGI; Expires=Fri, 26-Sep-2025 19:46:59 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-
-{
- "tag": "eeee",
- "tag_color": "#00ff00",
- "tag_description": "eeee"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff
deleted file mode 100644
index 2261d20aa7..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff
+++ /dev/null
@@ -1,15 +0,0 @@
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Content-Length: 77
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-Uha6unaW4OnQWiyBCgumrISPz'; style-src 'self' 'nonce-Uha6unaW4OnQWiyBCgumrISPz'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiODQzMWM2NjIzMjhlN2RhZjE0Y2JhZDQyZGJmMzEwYTdmMWViYzY5NyJ9.G4-gtA.uJpeS8gZ7_ApSzx9_9YXrVsord8; Expires=Fri, 26-Sep-2025 19:47:00 GMT; Secure; HttpOnly; Path=/
-
-{
- "tag": "ffff",
- "tag_color": "#00ff00",
- "tag_description": "ffff"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg
deleted file mode 100644
index c18309ee1e..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg
+++ /dev/null
@@ -1,15 +0,0 @@
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-Content-Length: 77
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-osDml6SVKadPzyuwCkKx7Tzp3'; style-src 'self' 'nonce-osDml6SVKadPzyuwCkKx7Tzp3'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiZjUwMDIxYjVhOWE4ZDA2ZTFiMjlkMzRhM2U1N2U1NGFlZmMwZjE1ZSJ9.G4-gtA.67dL477zv1XhBvSAhCpCBM5Jk8Y; Expires=Fri, 26-Sep-2025 19:47:00 GMT; Secure; HttpOnly; Path=/
-
-{
- "tag": "gggg",
- "tag_color": "#0000ff",
- "tag_description": "gggg"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fhhhh b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fhhhh
deleted file mode 100644
index 92888037b2..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fhhhh
+++ /dev/null
@@ -1,15 +0,0 @@
-Content-Length: 77
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-lDad6e2jG7okmGyGV9ITt9k8w'; style-src 'self' 'nonce-lDad6e2jG7okmGyGV9ITt9k8w'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiYjhkZTg4NzJlYjkzMGM5ODZkOTZlNzYyZTJlMWE1ZWEyODk5MzU3NCJ9.G4-gtA.cWHYAAufMpUOgFp0oG7rpf_2k-0; Expires=Fri, 26-Sep-2025 19:47:00 GMT; Secure; HttpOnly; Path=/
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-
-{
- "tag": "hhhh",
- "tag_color": "#0000ff",
- "tag_description": "hhhh"
-}
diff --git a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags
deleted file mode 100644
index c648a8f338..0000000000
--- a/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags
+++ /dev/null
@@ -1,23 +0,0 @@
-X-Content-Type-Options: nosniff
-Referrer-Policy: same-origin
-Content-Length: 142
-Content-Type: application/json
-X-Xss-Protection: 1; mode=block
-X-Frame-Options: ALLOW-FROM https://pagure.io/
-Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
-Content-Security-Policy: default-src 'self';script-src 'self' 'nonce-Kt2w1Q1kvLm2O7sDAV0XtpAWM'; style-src 'self' 'nonce-Kt2w1Q1kvLm2O7sDAV0XtpAWM'; object-src 'none';base-uri 'self';img-src 'self' https:;connect-src 'self' https://pagure.io:8088;frame-src https://docs.pagure.org;frame-ancestors https://pagure.io;
-Set-Cookie: pagure=eyJfcGVybWFuZW50Ijp0cnVlLCJjc3JmX3Rva2VuIjoiN2QyN2Y0NDIyM2ViZGJhZWRhMTg3NTc2NGY3MWYzMDVmOTgzNmI5ZSJ9.G4-gsg.Pl5rcBYRhcmWvmmUa6D-05E20KI; Expires=Fri, 26-Sep-2025 19:46:58 GMT; Secure; HttpOnly; Path=/
-
-{
- "tags": [
- "aaaa",
- "bbbb",
- "cccc",
- "dddd",
- "eeee",
- "ffff",
- "gggg",
- "hhhh"
- ],
- "total_tags": 8
-}
diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go
index 514b7c3969..6d871ad5ff 100644
--- a/services/mirror/mirror.go
+++ b/services/mirror/mirror.go
@@ -5,7 +5,7 @@ package mirror
import (
"context"
- "errors"
+ "fmt"
quota_model "forgejo.org/models/quota"
repo_model "forgejo.org/models/repo"
@@ -31,7 +31,7 @@ func doMirrorSync(ctx context.Context, req *SyncRequest) {
}
}
-var errLimit = errors.New("reached limit")
+var errLimit = fmt.Errorf("reached limit")
// Update checks and updates mirror repositories.
func Update(ctx context.Context, pullLimit, pushLimit int) error {
@@ -70,7 +70,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
// Check we've not been cancelled
select {
case <-ctx.Done():
- return errors.New("aborted")
+ return fmt.Errorf("aborted")
default:
}
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index c46323f283..a63cbcf40c 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -244,24 +244,6 @@ func pruneBrokenReferences(ctx context.Context,
return pruneErr
}
-// checkRecoverableSyncError takes an error message from a git fetch command and returns false if it should be a fatal/blocking error
-func checkRecoverableSyncError(stderrMessage string) bool {
- switch {
- case strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken"):
- return true
- case strings.Contains(stderrMessage, "remote error") && strings.Contains(stderrMessage, "not our ref"):
- return true
- case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "but expected"):
- return true
- case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "unable to resolve reference"):
- return true
- case strings.Contains(stderrMessage, "Unable to create") && strings.Contains(stderrMessage, ".lock"):
- return true
- default:
- return false
- }
-}
-
// runSync returns true if sync finished without error.
func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) {
repoPath := m.Repo.RepoPath()
@@ -304,7 +286,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
stdoutMessage := util.SanitizeCredentialURLs(stdout)
// Now check if the error is a resolve reference due to broken reference
- if checkRecoverableSyncError(stderr) {
+ if strings.Contains(stderr, "unable to resolve reference") && strings.Contains(stderr, "reference broken") {
log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
err = nil
@@ -355,15 +337,6 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
return nil, false
}
- if m.LFS && setting.LFS.StartServer {
- log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
- endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
- lfsClient := lfs.NewClient(endpoint, nil)
- if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
- log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
- }
- }
-
log.Trace("SyncMirrors [repo: %-v]: syncing branches...", m.Repo)
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to synchronize branches: %v", m.Repo, err)
@@ -373,6 +346,15 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
if err = repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo); err != nil {
log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err)
}
+
+ if m.LFS && setting.LFS.StartServer {
+ log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
+ endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
+ lfsClient := lfs.NewClient(endpoint, nil)
+ if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
+ log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
+ }
+ }
gitRepo.Close()
log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
@@ -400,7 +382,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
stdoutMessage := util.SanitizeCredentialURLs(stdout)
// Now check if the error is a resolve reference due to broken reference
- if checkRecoverableSyncError(stderrMessage) {
+ if strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken") {
log.Warn("SyncMirrors [repo: %-v Wiki]: failed to update mirror wiki repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
err = nil
diff --git a/services/mirror/mirror_pull_test.go b/services/mirror/mirror_pull_test.go
deleted file mode 100644
index 97859be5b0..0000000000
--- a/services/mirror/mirror_pull_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package mirror
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func Test_parseRemoteUpdateOutput(t *testing.T) {
- output := `
- * [new tag] v0.1.8 -> v0.1.8
- * [new branch] master -> origin/master
- - [deleted] (none) -> origin/test1
- - [deleted] (none) -> tag1
- + f895a1e...957a993 test2 -> origin/test2 (forced update)
- 957a993..a87ba5f test3 -> origin/test3
- * [new ref] refs/pull/26595/head -> refs/pull/26595/head
- * [new ref] refs/pull/26595/merge -> refs/pull/26595/merge
- e0639e38fb..6db2410489 refs/pull/25873/head -> refs/pull/25873/head
- + 1c97ebc746...976d27d52f refs/pull/25873/merge -> refs/pull/25873/merge (forced update)
-`
- results := parseRemoteUpdateOutput(output, "origin")
- assert.Len(t, results, 10)
- assert.Equal(t, "refs/tags/v0.1.8", results[0].refName.String())
- assert.Equal(t, gitShortEmptySha, results[0].oldCommitID)
- assert.Empty(t, results[0].newCommitID)
-
- assert.Equal(t, "refs/heads/master", results[1].refName.String())
- assert.Equal(t, gitShortEmptySha, results[1].oldCommitID)
- assert.Empty(t, results[1].newCommitID)
-
- assert.Equal(t, "refs/heads/test1", results[2].refName.String())
- assert.Empty(t, results[2].oldCommitID)
- assert.Equal(t, gitShortEmptySha, results[2].newCommitID)
-
- assert.Equal(t, "refs/tags/tag1", results[3].refName.String())
- assert.Empty(t, results[3].oldCommitID)
- assert.Equal(t, gitShortEmptySha, results[3].newCommitID)
-
- assert.Equal(t, "refs/heads/test2", results[4].refName.String())
- assert.Equal(t, "f895a1e", results[4].oldCommitID)
- assert.Equal(t, "957a993", results[4].newCommitID)
-
- assert.Equal(t, "refs/heads/test3", results[5].refName.String())
- assert.Equal(t, "957a993", results[5].oldCommitID)
- assert.Equal(t, "a87ba5f", results[5].newCommitID)
-
- assert.Equal(t, "refs/pull/26595/head", results[6].refName.String())
- assert.Equal(t, gitShortEmptySha, results[6].oldCommitID)
- assert.Empty(t, results[6].newCommitID)
-
- assert.Equal(t, "refs/pull/26595/merge", results[7].refName.String())
- assert.Equal(t, gitShortEmptySha, results[7].oldCommitID)
- assert.Empty(t, results[7].newCommitID)
-
- assert.Equal(t, "refs/pull/25873/head", results[8].refName.String())
- assert.Equal(t, "e0639e38fb", results[8].oldCommitID)
- assert.Equal(t, "6db2410489", results[8].newCommitID)
-
- assert.Equal(t, "refs/pull/25873/merge", results[9].refName.String())
- assert.Equal(t, "1c97ebc746", results[9].oldCommitID)
- assert.Equal(t, "976d27d52f", results[9].newCommitID)
-}
-
-func Test_checkRecoverableSyncError(t *testing.T) {
- cases := []struct {
- recoverable bool
- message string
- }{
- // A race condition in http git-fetch where certain refs were listed on the remote and are no longer there, would exit status 128
- {true, "fatal: remote error: upload-pack: not our ref 988881adc9fc3655077dc2d4d757d480b5ea0e11"},
- // A race condition where a local gc/prune removes a named ref during a git-fetch would exit status 1
- {true, "cannot lock ref 'refs/pull/123456/merge': unable to resolve reference 'refs/pull/134153/merge'"},
- // A race condition in http git-fetch where named refs were listed on the remote and are no longer there
- {true, "error: cannot lock ref 'refs/remotes/origin/foo': unable to resolve reference 'refs/remotes/origin/foo': reference broken"},
- // A race condition in http git-fetch where named refs were force-pushed during the update, would exit status 128
- {true, "error: cannot lock ref 'refs/pull/123456/merge': is at 988881adc9fc3655077dc2d4d757d480b5ea0e11 but expected 7f894307ffc9553edbd0b671cab829786866f7b2"},
- // A race condition with other local git operations, such as git-maintenance, would exit status 128 (well, "Unable" the "U" is uppercase)
- {true, "fatal: Unable to create '/data/gitea-repositories/foo-org/bar-repo.git/./objects/info/commit-graphs/commit-graph-chain.lock': File exists."},
- // Missing or unauthorized credentials, would exit status 128
- {false, "fatal: Authentication failed for 'https://example.com/foo-does-not-exist/bar.git/'"},
- // A non-existent remote repository, would exit status 128
- {false, "fatal: Could not read from remote repository."},
- // A non-functioning proxy, would exit status 128
- {false, "fatal: unable to access 'https://example.com/foo-does-not-exist/bar.git/': Failed to connect to configured-https-proxy port 1080 after 0 ms: Couldn't connect to server"},
- }
-
- for _, c := range cases {
- assert.Equal(t, c.recoverable, checkRecoverableSyncError(c.message), "test case: %s", c.message)
- }
-}
diff --git a/services/mirror/mirror_push.go b/services/mirror/mirror_push.go
index fdd02dedea..11b8ad459a 100644
--- a/services/mirror/mirror_push.go
+++ b/services/mirror/mirror_push.go
@@ -33,22 +33,19 @@ var AddPushMirrorRemote = addPushMirrorRemote
func addPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error {
addRemoteAndConfig := func(addr, path string) error {
- var cmd *git.Command
- if m.BranchFilter == "" {
- cmd = git.NewCommand(ctx, "remote", "add", "--mirror").AddDynamicArguments(m.RemoteName, addr)
- } else {
- cmd = git.NewCommand(ctx, "remote", "add").AddDynamicArguments(m.RemoteName, addr)
- }
+ cmd := git.NewCommand(ctx, "remote", "add", "--mirror=push").AddDynamicArguments(m.RemoteName, addr)
if strings.Contains(addr, "://") && strings.Contains(addr, "@") {
- cmd.SetDescription(fmt.Sprintf("remote add %s %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path))
+ cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path))
} else {
- cmd.SetDescription(fmt.Sprintf("remote add %s %s [repo_path: %s]", m.RemoteName, addr, path))
+ cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, addr, path))
}
if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
- err := addRemotePushRefSpecs(ctx, path, m)
- if err != nil {
+ if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
+ return err
+ }
+ if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
return nil
@@ -70,49 +67,6 @@ func addPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr str
return nil
}
-func addRemotePushRefSpecs(ctx context.Context, path string, m *repo_model.PushMirror) error {
- if m.BranchFilter == "" {
- // If there is no branch filter, set the push refspecs to mirror all branches and tags.
- if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/heads/*:refs/heads/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
- return err
- }
- } else {
- branches := strings.SplitSeq(m.BranchFilter, ",")
- for branch := range branches {
- branch = strings.TrimSpace(branch)
- if branch == "" {
- continue
- }
- refspec := fmt.Sprintf("+refs/heads/%s:refs/heads/%s", branch, branch)
- if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", refspec).RunStdString(&git.RunOpts{Dir: path}); err != nil {
- return err
- }
- }
- }
- if _, _, err := git.NewCommand(ctx, "config", "--add").AddDynamicArguments("remote."+m.RemoteName+".push", "+refs/tags/*:refs/tags/*").RunStdString(&git.RunOpts{Dir: path}); err != nil {
- return err
- }
- return nil
-}
-
-func UpdatePushMirrorBranchFilter(ctx context.Context, m *repo_model.PushMirror) error {
- path := m.Repo.RepoPath()
-
- // First, remove all existing push refspecs for this remote
- cmd := git.NewCommand(ctx, "config", "--unset-all").AddDynamicArguments("remote." + m.RemoteName + ".push")
- if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil {
- // Ignore error if the key doesn't exist
- if !strings.Contains(err.Error(), "does not exist") {
- return err
- }
- }
- err := addRemotePushRefSpecs(ctx, path, m)
- if err != nil {
- return err
- }
- return nil
-}
-
// RemovePushMirrorRemote removes the push mirror remote.
func RemovePushMirrorRemote(ctx context.Context, m *repo_model.PushMirror) error {
cmd := git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(m.RemoteName)
@@ -258,6 +212,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
return util.SanitizeErrorCredentialURLs(err)
}
+
return nil
}
diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go
new file mode 100644
index 0000000000..76632b6872
--- /dev/null
+++ b/services/mirror/mirror_test.go
@@ -0,0 +1,66 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package mirror
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_parseRemoteUpdateOutput(t *testing.T) {
+ output := `
+ * [new tag] v0.1.8 -> v0.1.8
+ * [new branch] master -> origin/master
+ - [deleted] (none) -> origin/test1
+ - [deleted] (none) -> tag1
+ + f895a1e...957a993 test2 -> origin/test2 (forced update)
+ 957a993..a87ba5f test3 -> origin/test3
+ * [new ref] refs/pull/26595/head -> refs/pull/26595/head
+ * [new ref] refs/pull/26595/merge -> refs/pull/26595/merge
+ e0639e38fb..6db2410489 refs/pull/25873/head -> refs/pull/25873/head
+ + 1c97ebc746...976d27d52f refs/pull/25873/merge -> refs/pull/25873/merge (forced update)
+`
+ results := parseRemoteUpdateOutput(output, "origin")
+ assert.Len(t, results, 10)
+ 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)
+
+ assert.EqualValues(t, "refs/heads/master", results[1].refName.String())
+ assert.EqualValues(t, gitShortEmptySha, results[1].oldCommitID)
+ assert.EqualValues(t, "", results[1].newCommitID)
+
+ assert.EqualValues(t, "refs/heads/test1", results[2].refName.String())
+ assert.EqualValues(t, "", results[2].oldCommitID)
+ assert.EqualValues(t, gitShortEmptySha, results[2].newCommitID)
+
+ assert.EqualValues(t, "refs/tags/tag1", results[3].refName.String())
+ assert.EqualValues(t, "", results[3].oldCommitID)
+ assert.EqualValues(t, gitShortEmptySha, results[3].newCommitID)
+
+ assert.EqualValues(t, "refs/heads/test2", results[4].refName.String())
+ assert.EqualValues(t, "f895a1e", results[4].oldCommitID)
+ assert.EqualValues(t, "957a993", results[4].newCommitID)
+
+ 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/26595/head", results[6].refName.String())
+ assert.EqualValues(t, gitShortEmptySha, results[6].oldCommitID)
+ assert.EqualValues(t, "", results[6].newCommitID)
+
+ assert.EqualValues(t, "refs/pull/26595/merge", results[7].refName.String())
+ assert.EqualValues(t, gitShortEmptySha, results[7].oldCommitID)
+ assert.EqualValues(t, "", results[7].newCommitID)
+
+ assert.EqualValues(t, "refs/pull/25873/head", results[8].refName.String())
+ assert.EqualValues(t, "e0639e38fb", results[8].oldCommitID)
+ assert.EqualValues(t, "6db2410489", results[8].newCommitID)
+
+ assert.EqualValues(t, "refs/pull/25873/merge", results[9].refName.String())
+ assert.EqualValues(t, "1c97ebc746", results[9].oldCommitID)
+ assert.EqualValues(t, "976d27d52f", results[9].newCommitID)
+}
diff --git a/services/moderation/main_test.go b/services/moderation/main_test.go
deleted file mode 100644
index 3a268260d2..0000000000
--- a/services/moderation/main_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package moderation
-
-import (
- "testing"
-
- "forgejo.org/models/unittest"
-
- _ "forgejo.org/models/forgefed"
- _ "forgejo.org/models/moderation"
-)
-
-func TestMain(m *testing.M) {
- unittest.MainTest(m)
-}
diff --git a/services/moderation/moderating.go b/services/moderation/moderating.go
deleted file mode 100644
index f329070963..0000000000
--- a/services/moderation/moderating.go
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package moderation
-
-import (
- "forgejo.org/models/issues"
- "forgejo.org/models/moderation"
- "forgejo.org/models/repo"
- "forgejo.org/models/user"
- "forgejo.org/modules/json"
- "forgejo.org/modules/log"
- "forgejo.org/services/context"
-)
-
-// GetShadowCopyMap unmarshals the shadow copy raw value of the given abuse report and returns a list of pairs
-// (to be rendered when the report is reviewed by an admin).
-// If the report does not have a shadow copy ID or the raw value is empty, returns nil.
-// If the unmarshal fails a warning is added in the logs and returns nil.
-func GetShadowCopyMap(ctx *context.Context, ard *moderation.AbuseReportDetailed) []moderation.ShadowCopyField {
- if ard.ShadowCopyID.Valid && len(ard.ShadowCopyRawValue) > 0 {
- var data moderation.ShadowCopyData
-
- switch ard.ContentType {
- case moderation.ReportedContentTypeUser:
- data = new(user.UserData)
- case moderation.ReportedContentTypeRepository:
- data = new(repo.RepositoryData)
- case moderation.ReportedContentTypeIssue:
- data = new(issues.IssueData)
- case moderation.ReportedContentTypeComment:
- data = new(issues.CommentData)
- }
- if err := json.Unmarshal([]byte(ard.ShadowCopyRawValue), &data); err != nil {
- log.Warn("Unmarshal failed for shadow copy #%d. %v", ard.ShadowCopyID.Int64, err)
- return nil
- }
- return data.GetFieldsMap()
- }
- return nil
-}
diff --git a/services/moderation/reporting.go b/services/moderation/reporting.go
deleted file mode 100644
index 3d1bb5b32c..0000000000
--- a/services/moderation/reporting.go
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package moderation
-
-import (
- stdCtx "context"
- "errors"
- "time"
-
- "forgejo.org/models/db"
- "forgejo.org/models/issues"
- "forgejo.org/models/moderation"
- "forgejo.org/models/perm"
- access_model "forgejo.org/models/perm/access"
- repo_model "forgejo.org/models/repo"
- "forgejo.org/models/unit"
- "forgejo.org/models/user"
- "forgejo.org/modules/log"
- "forgejo.org/services/context"
-)
-
-var (
- ErrContentDoesNotExist = errors.New("the content to be reported does not exist")
- ErrDoerNotAllowed = errors.New("doer not allowed to access the content to be reported")
-)
-
-// CanReport checks if doer has access to the content they are reporting
-// (user, organization, repository, issue, pull request or comment).
-// When reporting repositories the user should have at least read access to any repo unit type.
-// When reporting issues, pull requests or comments the user should have at least read access
-// to 'TypeIssues', respectively 'TypePullRequests' unit for the repository where the content belongs.
-// When reporting users or organizations doer should be able to view the reported entity.
-func CanReport(ctx context.Context, doer *user.User, contentType moderation.ReportedContentType, contentID int64) (bool, error) {
- hasAccess := false
- var issueID int64
- var repoID int64
- unitType := unit.TypeInvalid // used when checking access for issues, pull requests or comments
-
- if contentType == moderation.ReportedContentTypeUser {
- reportedUser, err := user.GetUserByID(ctx, contentID)
- if err != nil {
- if user.IsErrUserNotExist(err) {
- log.Warn("User #%d wanted to report user #%d but it does not exist.", doer.ID, contentID)
- return false, ErrContentDoesNotExist
- }
- return false, err
- }
-
- hasAccess = user.IsUserVisibleToViewer(ctx, reportedUser, ctx.Doer)
- if !hasAccess {
- log.Warn("User #%d wanted to report user/org #%d but they are not able to see that profile.", doer.ID, contentID)
- return false, ErrDoerNotAllowed
- }
- } else {
- // for comments and issues/pulls we need to get the parent repository
- switch contentType {
- case moderation.ReportedContentTypeComment:
- comment, err := issues.GetCommentByID(ctx, contentID)
- if err != nil {
- if issues.IsErrCommentNotExist(err) {
- log.Warn("User #%d wanted to report comment #%d but it does not exist.", doer.ID, contentID)
- return false, ErrContentDoesNotExist
- }
- return false, err
- }
- if !comment.Type.HasContentSupport() {
- // this is not a comment with text and/or attachments
- log.Warn("User #%d wanted to report comment #%d but it is not a comment with content.", doer.ID, contentID)
- return false, nil
- }
- issueID = comment.IssueID
- case moderation.ReportedContentTypeIssue:
- issueID = contentID
- case moderation.ReportedContentTypeRepository:
- repoID = contentID
- }
-
- if issueID > 0 {
- issue, err := issues.GetIssueByID(ctx, issueID)
- if err != nil {
- if issues.IsErrIssueNotExist(err) {
- log.Warn("User #%d wanted to report issue #%d (or one of its comments) but it does not exist.", doer.ID, issueID)
- return false, ErrContentDoesNotExist
- }
- return false, err
- }
-
- repoID = issue.RepoID
- if issue.IsPull {
- unitType = unit.TypePullRequests
- } else {
- unitType = unit.TypeIssues
- }
- }
-
- if repoID > 0 {
- repo, err := repo_model.GetRepositoryByID(ctx, repoID)
- if err != nil {
- if repo_model.IsErrRepoNotExist(err) {
- log.Warn("User #%d wanted to report repository #%d (or one of its issues / comments) but it does not exist.", doer.ID, repoID)
- return false, ErrContentDoesNotExist
- }
- return false, err
- }
-
- if issueID > 0 {
- // for comments and issues/pulls doer should have at least read access to the corresponding repo unit (issues, respectively pull requests)
- hasAccess, err = access_model.HasAccessUnit(ctx, doer, repo, unitType, perm.AccessModeRead)
- if err != nil {
- return false, err
- } else if !hasAccess {
- log.Warn("User #%d wanted to report issue #%d or one of its comments from repository #%d but they don't have access to it.", doer.ID, issueID, repoID)
- return false, ErrDoerNotAllowed
- }
- } else {
- // for repositories doer should have at least read access to at least one repo unit
- perm, err := access_model.GetUserRepoPermission(ctx, repo, doer)
- if err != nil {
- return false, err
- }
- hasAccess = perm.CanReadAny(unit.AllRepoUnitTypes...)
- if !hasAccess {
- log.Warn("User #%d wanted to report repository #%d but they don't have access to it.", doer.ID, repoID)
- return false, ErrDoerNotAllowed
- }
- }
- }
- }
-
- return hasAccess, nil
-}
-
-// RemoveResolvedReports removes resolved reports
-func RemoveResolvedReports(ctx stdCtx.Context, keepReportsFor time.Duration) error {
- log.Trace("Doing: RemoveResolvedReports")
-
- if keepReportsFor <= 0 {
- return nil
- }
-
- err := db.WithTx(ctx, func(ctx stdCtx.Context) error {
- resolvedReports, err := moderation.GetResolvedReports(ctx, keepReportsFor)
- if err != nil {
- return err
- }
-
- for _, report := range resolvedReports {
- _, err := db.GetEngine(ctx).ID(report.ID).Delete(&moderation.AbuseReport{})
- if err != nil {
- return err
- }
-
- if report.ShadowCopyID.Valid {
- _, err := db.GetEngine(ctx).ID(report.ShadowCopyID).Delete(&moderation.AbuseReportShadowCopy{})
- if err != nil {
- return err
- }
- }
- }
-
- return nil
- })
- if err != nil {
- return err
- }
-
- log.Trace("Finished: RemoveResolvedReports")
- return nil
-}
diff --git a/services/moderation/reporting_test.go b/services/moderation/reporting_test.go
deleted file mode 100644
index 70925bf184..0000000000
--- a/services/moderation/reporting_test.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package moderation
-
-import (
- "testing"
- "time"
-
- "forgejo.org/models/db"
- report_model "forgejo.org/models/moderation"
- "forgejo.org/models/unittest"
- "forgejo.org/modules/timeutil"
-
- "github.com/stretchr/testify/require"
-)
-
-func TestRemoveResolvedReportsWhenNoTimeSet(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- // reportAge needs to be an int64 to match what timeutil.Day expects so we cast the value
- reportAge := int64(20)
- resolvedReport := &report_model.AbuseReport{
- Status: report_model.ReportStatusTypeHandled,
- ReporterID: 1, ContentType: report_model.ReportedContentTypeRepository,
- ContentID: 2, Category: report_model.AbuseCategoryTypeOther,
- CreatedUnix: timeutil.TimeStampNow(),
- ResolvedUnix: timeutil.TimeStamp(time.Now().Unix() - timeutil.Day*reportAge),
- }
- _, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(resolvedReport)
- require.NoError(t, err)
-
- // No reports should be deleted when the default time to keep is 0
- err = RemoveResolvedReports(db.DefaultContext, time.Second*0)
- require.NoError(t, err)
- unittest.AssertExistsIf(t, true, resolvedReport)
-}
-
-func TestRemoveResolvedReportsWhenMatchTimeSet(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- // keepReportsFor needs to an int64 to match what timeutil.Day expects so we cast the value
- keepReportsFor := int64(4)
- resolvedReport := &report_model.AbuseReport{
- Status: report_model.ReportStatusTypeHandled,
- ReporterID: 1, ContentType: report_model.ReportedContentTypeRepository,
- ContentID: 2, Category: report_model.AbuseCategoryTypeOther,
- CreatedUnix: timeutil.TimeStampNow(),
- ResolvedUnix: timeutil.TimeStamp(time.Now().Unix() - timeutil.Day*keepReportsFor),
- }
-
- _, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(resolvedReport)
- require.NoError(t, err)
-
- // Report should be deleted when older than the default time to keep
- err = RemoveResolvedReports(db.DefaultContext, time.Second*4)
- require.NoError(t, err)
- unittest.AssertExistsIf(t, false, resolvedReport)
-}
-
-func TestRemoveResolvedReportsWhenTimeSetButReportNew(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- resolvedReport := &report_model.AbuseReport{
- Status: report_model.ReportStatusTypeHandled,
- ReporterID: 1, ContentType: report_model.ReportedContentTypeRepository,
- ContentID: 2, Category: report_model.AbuseCategoryTypeOther,
- CreatedUnix: timeutil.TimeStampNow(),
- ResolvedUnix: timeutil.TimeStampNow(),
- }
- _, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(resolvedReport)
- require.NoError(t, err)
-
- // Report should not be deleted when newer than the default time to keep
- err = RemoveResolvedReports(db.DefaultContext, time.Second*4)
- require.NoError(t, err)
- unittest.AssertExistsIf(t, true, resolvedReport)
-}
-
-func TestDoesNotRemoveOpenReports(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- // keepReportsFor needs to an int64 to match what timeutil.Day expects so we cast the value
- keepReportsFor := int64(4)
- resolvedReport := &report_model.AbuseReport{
- Status: report_model.ReportStatusTypeOpen,
- ReporterID: 1, ContentType: report_model.ReportedContentTypeRepository,
- ContentID: 2, Category: report_model.AbuseCategoryTypeOther,
- CreatedUnix: timeutil.TimeStampNow(),
- ResolvedUnix: timeutil.TimeStamp(time.Now().Unix() - timeutil.Day*keepReportsFor),
- }
-
- _, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(resolvedReport)
- require.NoError(t, err)
-
- // Report should not be deleted when open
- // and older than the default time to keep
- err = RemoveResolvedReports(db.DefaultContext, time.Second*4)
- require.NoError(t, err)
- unittest.AssertExistsIf(t, true, resolvedReport)
-}
diff --git a/services/notify/notifier.go b/services/notify/notifier.go
index 4d88a7ab95..00f98942d9 100644
--- a/services/notify/notifier.go
+++ b/services/notify/notifier.go
@@ -6,7 +6,6 @@ package notify
import (
"context"
- actions_model "forgejo.org/models/actions"
issues_model "forgejo.org/models/issues"
packages_model "forgejo.org/models/packages"
repo_model "forgejo.org/models/repo"
@@ -77,6 +76,4 @@ type Notifier interface {
PackageDelete(ctx context.Context, doer *user_model.User, pd *packages_model.PackageDescriptor)
ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository)
-
- ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun)
}
diff --git a/services/notify/notify.go b/services/notify/notify.go
index 02c18272cb..fb30dfb609 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -6,7 +6,6 @@ package notify
import (
"context"
- actions_model "forgejo.org/models/actions"
issues_model "forgejo.org/models/issues"
packages_model "forgejo.org/models/packages"
repo_model "forgejo.org/models/repo"
@@ -375,15 +374,3 @@ func ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) {
notifier.ChangeDefaultBranch(ctx, repo)
}
}
-
-// ActionRunNowDone notifies that the old status priorStatus with (priorStatus.isDone() == false) of an ActionRun changed to run.Status with (run.Status.isDone() == true)
-// run represents the new state of the ActionRun.
-// lastRun represents the ActionRun of the same workflow that finished before run.
-// lastRun might be nil (e.g. when the run is the first for this workflow). It is the last run of the same workflow for the same repo.
-// It can be used to figure out if a successful run follows a failed one.
-// Both run and lastRun need their attributes loaded.
-func ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) {
- for _, notifier := range notifiers {
- notifier.ActionRunNowDone(ctx, run, priorStatus, lastRun)
- }
-}
diff --git a/services/notify/null.go b/services/notify/null.go
index 9c76e5cbd3..7182e69abb 100644
--- a/services/notify/null.go
+++ b/services/notify/null.go
@@ -6,7 +6,6 @@ package notify
import (
"context"
- actions_model "forgejo.org/models/actions"
issues_model "forgejo.org/models/issues"
packages_model "forgejo.org/models/packages"
repo_model "forgejo.org/models/repo"
@@ -212,7 +211,3 @@ func (*NullNotifier) PackageDelete(ctx context.Context, doer *user_model.User, p
// ChangeDefaultBranch places a place holder function
func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.Repository) {
}
-
-// ActionRunNowDone places a place holder function
-func (*NullNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) {
-}
diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go
index dd66c7d74e..9435887a46 100644
--- a/services/packages/alpine/repository.go
+++ b/services/packages/alpine/repository.go
@@ -258,7 +258,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
privPem, _ := pem.Decode([]byte(priv))
if privPem == nil {
- return errors.New("failed to decode private key pem")
+ return fmt.Errorf("failed to decode private key pem")
}
privKey, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
diff --git a/services/packages/alt/repository.go b/services/packages/alt/repository.go
index 9693f4322e..317862da9d 100644
--- a/services/packages/alt/repository.go
+++ b/services/packages/alt/repository.go
@@ -714,23 +714,21 @@ func buildRelease(ctx context.Context, pv *packages_model.PackageVersion, pfs []
for architecture := range architectures.Seq() {
version := time.Now().Unix()
label := setting.AppName
- origin := setting.AppName
- archive := setting.AppName
-
- data := fmt.Sprintf(`Archive: %s
+ data := fmt.Sprintf(`Archive: Alt Linux Team
Component: classic
Version: %d
-Origin: %s
+Origin: Alt Linux Team
Label: %s
Architecture: %s
NotAutomatic: false
`,
- archive, version, origin, label, architecture)
+ version, label, architecture)
fileInfo, err := addReleaseAsFileToRepo(ctx, pv, "release.classic", data, group, architecture)
if err != nil {
return err
}
+ origin := setting.AppName
codename := time.Now().Unix()
date := time.Now().UTC().Format(time.RFC1123)
@@ -746,7 +744,7 @@ NotAutomatic: false
data = fmt.Sprintf(`Origin: %s
Label: %s
-Suite: Unknown
+Suite: Sisyphus
Codename: %d
Date: %s
Architectures: %s
diff --git a/services/packages/auth.go b/services/packages/auth.go
index 205125cf8b..ab2c347bc9 100644
--- a/services/packages/auth.go
+++ b/services/packages/auth.go
@@ -4,7 +4,6 @@
package packages
import (
- "errors"
"fmt"
"net/http"
"strings"
@@ -54,7 +53,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, auth_model.AccessTokenSc
parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 {
log.Error("split token failed: %s", h)
- return 0, "", errors.New("split token failed")
+ return 0, "", fmt.Errorf("split token failed")
}
token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (any, error) {
@@ -69,7 +68,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, auth_model.AccessTokenSc
c, ok := token.Claims.(*packageClaims)
if !token.Valid || !ok {
- return 0, "", errors.New("invalid token claim")
+ return 0, "", fmt.Errorf("invalid token claim")
}
return c.UserID, c.Scope, nil
diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go
index 03ad853216..7e821e2630 100644
--- a/services/packages/cleanup/cleanup.go
+++ b/services/packages/cleanup/cleanup.go
@@ -64,12 +64,13 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
PackageID: p.ID,
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
+ Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
if err != nil {
return fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err)
}
versionDeleted := false
- for _, pv := range pvs[pcr.KeepCount:] {
+ for _, pv := range pvs {
if pcr.Type == packages_model.TypeContainer {
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
return fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err)
@@ -121,24 +122,23 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
}
if anyVersionDeleted {
- switch pcr.Type {
- case packages_model.TypeDebian:
+ if pcr.Type == packages_model.TypeDebian {
if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
}
- case packages_model.TypeAlpine:
+ } else if pcr.Type == packages_model.TypeAlpine {
if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
}
- case packages_model.TypeRpm:
+ } else if pcr.Type == packages_model.TypeRpm {
if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
}
- case packages_model.TypeArch:
+ } else if pcr.Type == packages_model.TypeArch {
if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
}
- case packages_model.TypeAlt:
+ } else if pcr.Type == packages_model.TypeAlt {
if err := alt_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
return fmt.Errorf("CleanupRule [%d]: alt.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
}
diff --git a/services/packages/cleanup/cleanup_sha256_test.go b/services/packages/cleanup/cleanup_sha256_test.go
index efa254fc68..f26b98b4b0 100644
--- a/services/packages/cleanup/cleanup_sha256_test.go
+++ b/services/packages/cleanup/cleanup_sha256_test.go
@@ -78,7 +78,7 @@ func TestCleanupSHA256(t *testing.T) {
for range expected {
filtered = append(filtered, true)
}
- assert.Equal(t, filtered, logFiltered, expected)
+ assert.EqualValues(t, filtered, logFiltered, expected)
}
ancient := 1 * time.Hour
diff --git a/services/packages/container/blob_uploader.go b/services/packages/container/blob_uploader.go
index ffc47f3853..cc009d1f5c 100644
--- a/services/packages/container/blob_uploader.go
+++ b/services/packages/container/blob_uploader.go
@@ -92,7 +92,7 @@ func (u *BlobUploader) Append(ctx context.Context, r io.Reader) error {
u.BytesReceived += n
- u.HashStateBytes, err = u.MarshalBinary()
+ u.HashStateBytes, err = u.MultiHasher.MarshalBinary()
if err != nil {
return err
}
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 418ceab798..1232e5914f 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -127,12 +127,12 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
OwnerID: pvci.Owner.ID,
Type: pvci.PackageType,
Name: pvci.Name,
- LowerName: packages_model.ResolvePackageName(pvci.Name, pvci.PackageType),
+ LowerName: strings.ToLower(pvci.Name),
SemverCompatible: pvci.SemverCompatible,
}
var err error
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
- if errors.Is(err, packages_model.ErrDuplicatePackage) {
+ if err == packages_model.ErrDuplicatePackage {
packageCreated = false
} else {
log.Error("Error inserting package: %v", err)
@@ -208,7 +208,7 @@ func AddFileToExistingPackage(ctx context.Context, pvi *PackageInfo, pfci *Packa
// This method skips quota checks and should only be used for system-managed packages.
func AddFileToPackageVersionInternal(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, error) {
return addFileToPackageWrapper(ctx, func(ctx context.Context) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
- return addFileToPackageVersionUnchecked(ctx, pv, pfci, "")
+ return addFileToPackageVersionUnchecked(ctx, pv, pfci)
})
}
@@ -261,10 +261,10 @@ func addFileToPackageVersion(ctx context.Context, pv *packages_model.PackageVers
return nil, nil, false, err
}
- return addFileToPackageVersionUnchecked(ctx, pv, pfci, pvi.PackageType)
+ return addFileToPackageVersionUnchecked(ctx, pv, pfci)
}
-func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo, packageType packages_model.Type) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
+func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
log.Trace("Adding package file: %v, %s", pv.ID, pfci.Filename)
pb, exists, err := packages_model.GetOrInsertBlob(ctx, NewPackageBlob(pfci.Data))
@@ -304,7 +304,7 @@ func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.Pa
VersionID: pv.ID,
BlobID: pb.ID,
Name: pfci.Filename,
- LowerName: packages_model.ResolvePackageName(pfci.Filename, packageType),
+ LowerName: strings.ToLower(pfci.Filename),
CompositeKey: pfci.CompositeKey,
IsLead: pfci.IsLead,
}
diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go
index 26f34be2bc..961de7828f 100644
--- a/services/packages/rpm/repository.go
+++ b/services/packages/rpm/repository.go
@@ -26,10 +26,10 @@ import (
"forgejo.org/modules/util"
packages_service "forgejo.org/services/packages"
- "code.forgejo.org/forgejo/go-rpmutils"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
+ "github.com/sassoftware/go-rpmutils"
)
// GetOrCreateRepositoryVersion gets or creates the internal repository package
@@ -410,6 +410,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
files = append(files, f)
}
}
+ packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
packages = append(packages, &Package{
Type: "rpm",
Name: pd.Package.Name,
@@ -438,7 +439,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []
Archive: pd.FileMetadata.ArchiveSize,
},
Location: Location{
- Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture, pd.Package.Name, pd.Version.Version, pd.FileMetadata.Architecture),
+ Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
},
Format: Format{
License: pd.VersionMetadata.License,
diff --git a/services/pull/check.go b/services/pull/check.go
index c31d107605..d038b3d829 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -28,7 +28,6 @@ import (
"forgejo.org/modules/timeutil"
asymkey_service "forgejo.org/services/asymkey"
notify_service "forgejo.org/services/notify"
- shared_automerge "forgejo.org/services/shared/automerge"
)
// prPatchCheckerQueue represents a queue to handle update pull request tests
@@ -171,7 +170,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
// checkAndUpdateStatus checks if pull request is possible to leaving checking status,
// and set to be either conflict or mergeable.
-func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) bool {
+func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) {
// If status has not been changed to conflict by testPatch then we are mergeable
if pr.Status == issues_model.PullRequestStatusChecking {
pr.Status = issues_model.PullRequestStatusMergeable
@@ -185,15 +184,12 @@ func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) boo
if has {
log.Trace("Not updating status for %-v as it is due to be rechecked", pr)
- return false
+ return
}
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
log.Error("Update[%-v]: %v", pr, err)
- return false
}
-
- return true
}
// getMergeCommit checks if a pull request has been merged
@@ -343,22 +339,15 @@ func handler(items ...string) []string {
}
func testPR(id int64) {
- ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id))
- defer finished()
-
- if pr, updated := testPRProtected(ctx, id); pr != nil && updated {
- shared_automerge.AddToQueueIfMergeable(ctx, pr)
- }
-}
-
-func testPRProtected(ctx context.Context, id int64) (*issues_model.PullRequest, bool) {
pullWorkingPool.CheckIn(fmt.Sprint(id))
defer pullWorkingPool.CheckOut(fmt.Sprint(id))
+ ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("Test PR[%d] from patch checking queue", id))
+ defer finished()
pr, err := issues_model.GetPullRequestByID(ctx, id)
if err != nil {
log.Error("Unable to GetPullRequestByID[%d] for testPR: %v", id, err)
- return nil, false
+ return
}
log.Trace("Testing %-v", pr)
@@ -368,12 +357,12 @@ func testPRProtected(ctx context.Context, id int64) (*issues_model.PullRequest,
if pr.HasMerged {
log.Trace("%-v is already merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
- return nil, false
+ return
}
if manuallyMerged(ctx, pr) {
log.Trace("%-v is manually merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
- return nil, false
+ return
}
if err := TestPatch(pr); err != nil {
@@ -382,10 +371,9 @@ func testPRProtected(ctx context.Context, id int64) (*issues_model.PullRequest,
if err := pr.UpdateCols(ctx, "status"); err != nil {
log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err)
}
- return nil, false
+ return
}
-
- return pr, checkAndUpdateStatus(ctx, pr)
+ checkAndUpdateStatus(ctx, pr)
}
// CheckPRsForBaseBranch check all pulls with baseBrannch
@@ -404,14 +392,10 @@ func CheckPRsForBaseBranch(ctx context.Context, baseRepo *repo_model.Repository,
// Init runs the task queue to test all the checking status pull requests
func Init() error {
- if err := LoadMergeMessageTemplates(); err != nil {
- return err
- }
-
prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", handler)
if prPatchCheckerQueue == nil {
- return errors.New("unable to create pr_patch_checker queue")
+ return fmt.Errorf("unable to create pr_patch_checker queue")
}
go graceful.GetManager().RunWithCancel(prPatchCheckerQueue)
diff --git a/services/pull/check_test.go b/services/pull/check_test.go
index 9b7e1660bc..b965d90236 100644
--- a/services/pull/check_test.go
+++ b/services/pull/check_test.go
@@ -52,7 +52,7 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
select {
case id := <-idChan:
- assert.Equal(t, pr.ID, id)
+ assert.EqualValues(t, pr.ID, id)
case <-time.After(time.Second):
assert.FailNow(t, "Timeout: nothing was added to pullRequestQueue")
}
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index 0a95ea1152..3c864c8ef2 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -12,6 +12,7 @@ import (
"forgejo.org/models/db"
git_model "forgejo.org/models/git"
issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
"forgejo.org/modules/log"
"forgejo.org/modules/structs"
@@ -104,7 +105,7 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR
if pr.Flow == issues_model.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) {
return "", errors.New("head branch does not exist, can not merge")
}
- if pr.Flow == issues_model.PullRequestFlowAGit && !headGitRepo.IsReferenceExist(pr.GetGitRefName()) {
+ if pr.Flow == issues_model.PullRequestFlowAGit && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
return "", errors.New("head branch does not exist, can not merge")
}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index a2542176f0..9b0d632377 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -6,7 +6,6 @@ package pull
import (
"context"
- "errors"
"fmt"
"net/url"
"os"
@@ -14,7 +13,6 @@ import (
"regexp"
"strconv"
"strings"
- "unicode"
"forgejo.org/models"
"forgejo.org/models/db"
@@ -35,30 +33,6 @@ import (
notify_service "forgejo.org/services/notify"
)
-var mergeMessageTemplates = make(map[repo_model.MergeStyle]string, len(repo_model.MergeStyles))
-
-func LoadMergeMessageTemplates() error {
- // Load templates for all known merge styles
- for _, mergeStyle := range repo_model.MergeStyles {
- templateFilename := filepath.Join(
- setting.CustomPath,
- "default_merge_message",
- fmt.Sprintf("%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle))),
- )
-
- content, err := os.ReadFile(templateFilename)
- if err == nil {
- mergeMessageTemplates[mergeStyle] = string(content)
- } else if os.IsNotExist(err) {
- // The file no longer exists, so delete any previous content
- delete(mergeMessageTemplates, mergeStyle)
- } else {
- return err
- }
- }
- return nil
-}
-
// getMergeMessage composes the message used when merging a pull request.
func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle, extraVars map[string]string) (message, body string, err error) {
if err := pr.LoadBaseRepo(ctx); err != nil {
@@ -103,13 +77,6 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue
if _, ok := err.(git.ErrNotExist); ok {
templateContent, err = commit.GetFileContent(templateFilepathGitea, setting.Repository.PullRequest.DefaultMergeMessageSize)
}
-
- if _, ok := err.(git.ErrNotExist); ok {
- if preloadedContent, ok := mergeMessageTemplates[mergeStyle]; ok {
- templateContent, err = preloadedContent, nil
- }
- }
-
if err != nil {
if !git.IsErrNotExist(err) {
return "", "", err
@@ -201,68 +168,10 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr
return getMergeMessage(ctx, baseGitRepo, pr, mergeStyle, nil)
}
-func AddCommitMessageTrailer(message, tailerKey, tailerValue string) string {
- trailerLine := tailerKey + ": " + tailerValue
- message = strings.ReplaceAll(message, "\r\n", "\n")
- message = strings.ReplaceAll(message, "\r", "\n")
- if strings.Contains(message, "\n"+trailerLine+"\n") || strings.HasSuffix(message, "\n"+trailerLine) {
- return message
- }
-
- if !strings.HasSuffix(message, "\n") {
- message += "\n"
- }
- lastNewLine := strings.LastIndexByte(message[:len(message)-1], '\n')
- keyEnd := -1
- if lastNewLine != -1 {
- keyEnd = strings.IndexByte(message[lastNewLine:], ':')
- if keyEnd != -1 {
- keyEnd += lastNewLine
- }
- }
- var lastLineKey string
- if lastNewLine != -1 && keyEnd != -1 {
- lastLineKey = message[lastNewLine+1 : keyEnd]
- }
-
- isLikelyTrailerLine := lastLineKey != "" && unicode.IsUpper(rune(lastLineKey[0])) && strings.Contains(message, "-")
- for i := 0; isLikelyTrailerLine && i < len(lastLineKey); i++ {
- r := rune(lastLineKey[i])
- isLikelyTrailerLine = unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-'
- }
- if !strings.HasSuffix(message, "\n\n") && !isLikelyTrailerLine {
- message += "\n"
- }
- return message + trailerLine
-}
-
// Merge merges pull request to base repository.
// Caller should check PR is ready to be merged (review and status checks)
func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error {
- pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
- defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
-
- pr, err := issues_model.GetPullRequestByID(ctx, pr.ID)
- if err != nil {
- log.Error("Unable to load pull request itself: %v", err)
- return fmt.Errorf("unable to load pull request itself: %w", err)
- }
-
- if pr.HasMerged {
- return models.ErrPullRequestHasMerged{
- ID: pr.ID,
- IssueID: pr.IssueID,
- HeadRepoID: pr.HeadRepoID,
- BaseRepoID: pr.BaseRepoID,
- HeadBranch: pr.HeadBranch,
- BaseBranch: pr.BaseBranch,
- }
- }
-
- if err := pr.LoadIssue(ctx); err != nil {
- log.Error("Unable to load issue: %v", err)
- return fmt.Errorf("unable to load issue: %w", err)
- } else if err := pr.LoadBaseRepo(ctx); err != nil {
+ if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("Unable to load base repo: %v", err)
return fmt.Errorf("unable to load base repo: %w", err)
} else if err := pr.LoadHeadRepo(ctx); err != nil {
@@ -270,6 +179,9 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
return fmt.Errorf("unable to load head repo: %w", err)
}
+ pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
+ defer pullWorkingPool.CheckOut(fmt.Sprint(pr.ID))
+
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
@@ -606,13 +518,13 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if len(commitID) != objectFormat.FullLength() {
- return errors.New("Wrong commit ID")
+ return fmt.Errorf("Wrong commit ID")
}
commit, err := baseGitRepo.GetCommit(commitID)
if err != nil {
if git.IsErrNotExist(err) {
- return errors.New("Wrong commit ID")
+ return fmt.Errorf("Wrong commit ID")
}
return err
}
@@ -623,7 +535,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
return err
}
if !ok {
- return errors.New("Wrong commit ID")
+ return fmt.Errorf("Wrong commit ID")
}
pr.MergedCommitID = commitID
@@ -636,7 +548,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
if merged, err = pr.SetMerged(ctx); err != nil {
return err
} else if !merged {
- return errors.New("SetMerged failed")
+ return fmt.Errorf("SetMerged failed")
}
return nil
}); err != nil {
diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go
index 4598d57b7a..fb09515dbd 100644
--- a/services/pull/merge_prepare.go
+++ b/services/pull/merge_prepare.go
@@ -236,77 +236,10 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string, o
// rebaseTrackingOnToBase checks out the tracking branch as staging and rebases it on to the base branch
// if there is a conflict it will return a models.ErrRebaseConflicts
func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) error {
- // Create staging branch
- if err := git.NewCommand(ctx, "branch").AddDynamicArguments(stagingBranch, trackingBranch).
- Run(ctx.RunOpts()); err != nil {
- return fmt.Errorf(
- "unable to git branch tracking as staging in temp repo for %v: %w\n%s\n%s",
- ctx.pr, err,
- ctx.outbuf.String(),
- ctx.errbuf.String(),
- )
- }
- ctx.outbuf.Reset()
- ctx.errbuf.Reset()
-
- // If the pull request is zero commits behind, then no rebasing needs to be done.
- if ctx.pr.CommitsBehind == 0 {
- return nil
- }
-
- // Check git version for availability of git-replay. If it is available, we use
- // it for performance and to preserve unknown commit headers like the
- // "change-id" header used by Jujutsu and GitButler to track changes across
- // rebase, amend etc.
- if err := git.CheckGitVersionAtLeast("2.44"); err == nil {
- // Use git-replay for performance and to preserve unknown headers,
- // like the "change-id" header used by Jujutsu and GitButler.
- if err := git.NewCommand(ctx, "replay", "--onto").AddDynamicArguments(baseBranch).
- AddDynamicArguments(fmt.Sprintf("%s..%s", baseBranch, stagingBranch)).
- Run(ctx.RunOpts()); err != nil {
- // git-replay doesn't tell us which commit first created a merge conflict.
- // In order to preserve the quality of our error messages, fall back to
- // regular git-rebase.
- goto regular_rebase
- }
- // git-replay worked, stdout contains the instructions for update-ref
- updateRefInstructions := ctx.outbuf.String()
- opts := ctx.RunOpts()
- opts.Stdin = strings.NewReader(updateRefInstructions)
- if err := git.NewCommand(ctx, "update-ref", "--stdin").Run(opts); err != nil {
- return fmt.Errorf(
- "Failed to update ref for %v: %w\n%s\n%s",
- ctx.pr,
- err,
- ctx.outbuf.String(),
- ctx.errbuf.String(),
- )
- }
- // Checkout staging branch
- if err := git.NewCommand(ctx, "checkout").AddDynamicArguments(stagingBranch).
- Run(ctx.RunOpts()); err != nil {
- return fmt.Errorf(
- "unable to git checkout staging in temp repo for %v: %w\n%s\n%s",
- ctx.pr,
- err,
- ctx.outbuf.String(),
- ctx.errbuf.String(),
- )
- }
- ctx.outbuf.Reset()
- ctx.errbuf.Reset()
- return nil
- }
-
- // The available git version is too old to support git-replay, or git-replay
- // failed and we want to determine the first commit that produced a
- // merge-conflict. Fall back to regular rebase.
-regular_rebase:
-
// Checkout head branch
- if err := git.NewCommand(ctx, "checkout").AddDynamicArguments(stagingBranch).
+ if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch).
Run(ctx.RunOpts()); err != nil {
- return fmt.Errorf("unable to git checkout staging in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
+ return fmt.Errorf("unable to git checkout tracking as staging in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
}
ctx.outbuf.Reset()
ctx.errbuf.Reset()
diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go
index f655224c5e..1c6f734a25 100644
--- a/services/pull/merge_squash.go
+++ b/services/pull/merge_squash.go
@@ -5,6 +5,7 @@ package pull
import (
"fmt"
+ "strings"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
@@ -66,8 +67,10 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() {
// add trailer
- message = AddCommitMessageTrailer(message, "Co-authored-by", sig.String())
- message = AddCommitMessageTrailer(message, "Co-committed-by", sig.String()) // FIXME: this one should be removed, it is not really used or widely used
+ if !strings.Contains(message, fmt.Sprintf("Co-authored-by: %s", sig.String())) {
+ message += fmt.Sprintf("\nCo-authored-by: %s", sig.String())
+ }
+ message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String())
}
cmdCommit := git.NewCommand(ctx, "commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).
diff --git a/services/pull/merge_test.go b/services/pull/merge_test.go
index aa033b8cdb..6df6f55d46 100644
--- a/services/pull/merge_test.go
+++ b/services/pull/merge_test.go
@@ -4,22 +4,9 @@
package pull
import (
- "os"
- "path"
- "strings"
"testing"
- "forgejo.org/models"
- issues_model "forgejo.org/models/issues"
- repo_model "forgejo.org/models/repo"
- "forgejo.org/models/unittest"
- user_model "forgejo.org/models/user"
- "forgejo.org/modules/gitrepo"
- "forgejo.org/modules/setting"
- "forgejo.org/modules/test"
-
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
func Test_expandDefaultMergeMessage(t *testing.T) {
@@ -78,102 +65,3 @@ func Test_expandDefaultMergeMessage(t *testing.T) {
})
}
}
-
-func TestAddCommitMessageTailer(t *testing.T) {
- // add tailer for empty message
- assert.Equal(t, "\n\nTest-tailer: TestValue", AddCommitMessageTrailer("", "Test-tailer", "TestValue"))
-
- // add tailer for message without newlines
- assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title", "Test-tailer", "TestValue"))
- assert.Equal(t, "title\n\nNot tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nNot tailer: xxx", "Test-tailer", "TestValue"))
- assert.Equal(t, "title\n\nNotTailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nNotTailer: xxx", "Test-tailer", "TestValue"))
- assert.Equal(t, "title\n\nnot-tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nnot-tailer: xxx", "Test-tailer", "TestValue"))
-
- // add tailer for message with one EOL
- assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n", "Test-tailer", "TestValue"))
-
- // add tailer for message with two EOLs
- assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\n", "Test-tailer", "TestValue"))
-
- // add tailer for message with existing tailer (won't duplicate)
- assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTrailer("title\n\nTest-tailer: TestValue", "Test-tailer", "TestValue"))
- assert.Equal(t, "title\n\nTest-tailer: TestValue\n", AddCommitMessageTrailer("title\n\nTest-tailer: TestValue\n", "Test-tailer", "TestValue"))
-
- // add tailer for message with existing tailer and different value (will append)
- assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTrailer("title\n\nTest-tailer: v1", "Test-tailer", "v2"))
- assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTrailer("title\n\nTest-tailer: v1\n", "Test-tailer", "v2"))
-}
-
-func prepareLoadMergeMessageTemplates(targetDir string) error {
- for _, template := range []string{"MERGE", "REBASE", "REBASE-MERGE", "SQUASH", "MANUALLY-MERGED", "REBASE-UPDATE-ONLY"} {
- file, err := os.Create(path.Join(targetDir, template+"_TEMPLATE.md"))
- defer file.Close()
-
- if err == nil {
- _, err = file.WriteString("Contents for " + template)
- }
-
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func TestLoadMergeMessageTemplates(t *testing.T) {
- defer test.MockVariableValue(&setting.CustomPath, t.TempDir())()
- templateTemp := path.Join(setting.CustomPath, "default_merge_message")
-
- require.NoError(t, os.MkdirAll(templateTemp, 0o755))
- require.NoError(t, prepareLoadMergeMessageTemplates(templateTemp))
-
- testStyles := []repo_model.MergeStyle{
- repo_model.MergeStyleMerge,
- repo_model.MergeStyleRebase,
- repo_model.MergeStyleRebaseMerge,
- repo_model.MergeStyleSquash,
- repo_model.MergeStyleManuallyMerged,
- repo_model.MergeStyleRebaseUpdate,
- }
-
- // Load all templates
- require.NoError(t, LoadMergeMessageTemplates())
-
- // Check their correctness
- assert.Len(t, mergeMessageTemplates, len(testStyles))
- for _, mergeStyle := range testStyles {
- assert.Equal(t, "Contents for "+strings.ToUpper(string(mergeStyle)), mergeMessageTemplates[mergeStyle])
- }
-
- // Unload all templates
- require.NoError(t, os.RemoveAll(templateTemp))
- require.NoError(t, LoadMergeMessageTemplates())
- assert.Empty(t, mergeMessageTemplates)
-}
-
-func TestMergeMergedPR(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
- doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-
- require.NoError(t, pr.LoadBaseRepo(t.Context()))
-
- gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo)
- require.NoError(t, err)
- defer gitRepo.Close()
-
- assert.True(t, pr.HasMerged)
- pr.HasMerged = false
-
- err = Merge(t.Context(), pr, doer, gitRepo, repo_model.MergeStyleRebase, "", "I should not exist", false)
- require.Error(t, err)
- assert.True(t, models.IsErrPullRequestHasMerged(err))
-
- if mergeErr, ok := err.(models.ErrPullRequestHasMerged); ok {
- assert.Equal(t, pr.ID, mergeErr.ID)
- assert.Equal(t, pr.IssueID, mergeErr.IssueID)
- assert.Equal(t, pr.HeadBranch, mergeErr.HeadBranch)
- assert.Equal(t, pr.BaseBranch, mergeErr.BaseBranch)
- }
-}
diff --git a/services/pull/patch.go b/services/pull/patch.go
index 89581c916b..35d1b101e2 100644
--- a/services/pull/patch.go
+++ b/services/pull/patch.go
@@ -5,7 +5,7 @@
package pull
import (
- "bytes"
+ "bufio"
"context"
"fmt"
"io"
@@ -16,11 +16,14 @@ import (
"forgejo.org/models"
git_model "forgejo.org/models/git"
issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/container"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
"forgejo.org/modules/graceful"
"forgejo.org/modules/log"
"forgejo.org/modules/process"
+ "forgejo.org/modules/setting"
"forgejo.org/modules/util"
"github.com/gobwas/glob"
@@ -46,162 +49,66 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io
return nil
}
+var patchErrorSuffices = []string{
+ ": already exists in index",
+ ": patch does not apply",
+ ": already exists in working directory",
+ "unrecognized input",
+ ": No such file or directory",
+ ": does not exist in index",
+}
+
// TestPatch will test whether a simple patch will apply
func TestPatch(pr *issues_model.PullRequest) error {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().HammerContext(), fmt.Sprintf("TestPatch: %s", pr))
defer finished()
- testPatchCtx, err := testPatch(ctx, pr)
- testPatchCtx.close()
- return err
-}
-
-type testPatchContext struct {
- headRev string
- headIsCommitID bool
- baseRev string
- env []string
- gitRepo *git.Repository
- close func()
-}
-
-// LoadHeadRevision loads the necessary information to access the head revision.
-func (t *testPatchContext) LoadHeadRevision(ctx context.Context, pr *issues_model.PullRequest) error {
- // If AGit, then use HeadCommitID if set (AGit flow creates pull request),
- // otherwise use the pull request reference.
- if pr.Flow == issues_model.PullRequestFlowAGit {
- if len(pr.HeadCommitID) > 0 {
- t.headRev = pr.HeadCommitID
- t.headIsCommitID = true
- return nil
- }
- t.headRev = pr.GetGitRefName()
- return nil
- }
-
- // If it is within the same repository, simply return the branch name.
- if pr.BaseRepoID == pr.HeadRepoID {
- t.headRev = pr.GetGitHeadBranchRefName()
- return nil
- }
-
- // We are in Github flow, head and base repository are different.
- // Resolve the head branch to a commitID and return a Git alternate
- // environment for the head repository.
- gitRepo, err := git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
+ prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil {
+ if !git_model.IsErrBranchNotExist(err) {
+ log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
+ }
return err
}
+ defer cancel()
+
+ return testPatch(ctx, prCtx, pr)
+}
+
+func testPatch(ctx context.Context, prCtx *prContext, pr *issues_model.PullRequest) error {
+ gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
+ if err != nil {
+ return fmt.Errorf("OpenRepository: %w", err)
+ }
defer gitRepo.Close()
- headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadBranchRefName())
- if err != nil {
- return err
- }
-
- t.headRev = headCommitID
- t.headIsCommitID = true
- t.env = append(os.Environ(), `GIT_ALTERNATE_OBJECT_DIRECTORIES=`+pr.HeadRepo.RepoPath()+"/objects")
- return nil
-}
-
-// getTestPatchCtx constructs a new testpatch context for the given pull request.
-// If `onBare` is true, then the context will use the base repository that does
-// not contain a working tree. Otherwise a temprorary repository is created that
-// contains a working tree.
-func getTestPatchCtx(ctx context.Context, pr *issues_model.PullRequest, onBare bool) (*testPatchContext, error) {
- testPatchCtx := &testPatchContext{
- close: func() {},
- }
-
- if onBare {
- if err := pr.LoadBaseRepo(ctx); err != nil {
- return testPatchCtx, fmt.Errorf("LoadBaseRepo: %w", err)
- }
- if err := pr.LoadHeadRepo(ctx); err != nil {
- return testPatchCtx, fmt.Errorf("LoadHeadRepo: %w", err)
- }
-
- if err := testPatchCtx.LoadHeadRevision(ctx, pr); err != nil {
- return testPatchCtx, fmt.Errorf("LoadHeadRevision: %w", err)
- }
-
- gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
- if err != nil {
- return testPatchCtx, fmt.Errorf("OpenRepository: %w", err)
- }
-
- testPatchCtx.baseRev = git.BranchPrefix + pr.BaseBranch
- testPatchCtx.gitRepo = gitRepo
- testPatchCtx.close = func() {
- gitRepo.Close()
- }
- } else {
- prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
- if err != nil {
- return testPatchCtx, fmt.Errorf("createTemporaryRepoForPR: %w", err)
- }
- testPatchCtx.close = cancel
-
- gitRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
- if err != nil {
- return testPatchCtx, fmt.Errorf("OpenRepository: %w", err)
- }
-
- testPatchCtx.baseRev = git.BranchPrefix + baseBranch
- testPatchCtx.headRev = git.BranchPrefix + trackingBranch
- testPatchCtx.gitRepo = gitRepo
- testPatchCtx.close = func() {
- cancel()
- gitRepo.Close()
- }
- }
- return testPatchCtx, nil
-}
-
-func testPatch(ctx context.Context, pr *issues_model.PullRequest) (*testPatchContext, error) {
- testPatchCtx, err := getTestPatchCtx(ctx, pr, git.SupportGitMergeTree)
- if err != nil {
- return testPatchCtx, fmt.Errorf("getTestPatchCtx: %w", err)
- }
-
// 1. update merge base
- pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base").AddDashesAndList(testPatchCtx.baseRev, testPatchCtx.headRev).RunStdString(&git.RunOpts{Dir: testPatchCtx.gitRepo.Path, Env: testPatchCtx.env})
+ pr.MergeBase, _, err = git.NewCommand(ctx, "merge-base", "--", "base", "tracking").RunStdString(&git.RunOpts{Dir: prCtx.tmpBasePath})
if err != nil {
var err2 error
- pr.MergeBase, err2 = testPatchCtx.gitRepo.GetRefCommitID(testPatchCtx.baseRev)
+ pr.MergeBase, err2 = gitRepo.GetRefCommitID(git.BranchPrefix + "base")
if err2 != nil {
- return testPatchCtx, fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %w", err, err2)
+ return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %w", err, err2)
}
}
pr.MergeBase = strings.TrimSpace(pr.MergeBase)
-
- if testPatchCtx.headIsCommitID {
- pr.HeadCommitID = testPatchCtx.headRev
- } else {
- if pr.HeadCommitID, err = testPatchCtx.gitRepo.GetRefCommitID(testPatchCtx.headRev); err != nil {
- return testPatchCtx, fmt.Errorf("GetRefCommitID: can't find commit ID for head: %w", err)
- }
+ if pr.HeadCommitID, err = gitRepo.GetRefCommitID(git.BranchPrefix + "tracking"); err != nil {
+ return fmt.Errorf("GetBranchCommitID: can't find commit ID for head: %w", err)
}
- // If the head commit is equal to the merge base it roughly means that the
- // head commit is a parent of the base commit.
if pr.HeadCommitID == pr.MergeBase {
pr.Status = issues_model.PullRequestStatusAncestor
- return testPatchCtx, nil
+ return nil
}
// 2. Check for conflicts
- if conflicts, err := checkConflicts(ctx, pr, testPatchCtx); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty {
- if err != nil {
- return testPatchCtx, fmt.Errorf("checkConflicts: %w", err)
- }
- return testPatchCtx, nil
+ if conflicts, err := checkConflicts(ctx, pr, gitRepo, prCtx.tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty {
+ return err
}
// 3. Check for protected files changes
- if err = checkPullFilesProtection(ctx, pr, testPatchCtx); err != nil {
- return testPatchCtx, fmt.Errorf("checkPullFilesProtection: %v", err)
+ if err = checkPullFilesProtection(ctx, pr, gitRepo); err != nil {
+ return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
}
if len(pr.ChangedProtectedFiles) > 0 {
@@ -210,7 +117,7 @@ func testPatch(ctx context.Context, pr *issues_model.PullRequest) (*testPatchCon
pr.Status = issues_model.PullRequestStatusMergeable
- return testPatchCtx, nil
+ return nil
}
type errMergeConflict struct {
@@ -329,12 +236,12 @@ func attemptMerge(ctx context.Context, file *unmergedFile, tmpBasePath string, f
}
// AttemptThreeWayMerge will attempt to three way merge using git read-tree and then follow the git merge-one-file algorithm to attempt to resolve basic conflicts
-func AttemptThreeWayMerge(ctx context.Context, gitRepo *git.Repository, base, ours, theirs, description string) (bool, []string, error) {
+func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repository, base, ours, theirs, description string) (bool, []string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// First we use read-tree to do a simple three-way merge
- if _, _, err := git.NewCommand(ctx, "read-tree", "-m").AddDynamicArguments(base, ours, theirs).RunStdString(&git.RunOpts{Dir: gitRepo.Path}); err != nil {
+ if _, _, err := git.NewCommand(ctx, "read-tree", "-m").AddDynamicArguments(base, ours, theirs).RunStdString(&git.RunOpts{Dir: gitPath}); err != nil {
log.Error("Unable to run read-tree -m! Error: %v", err)
return false, nil, fmt.Errorf("unable to run read-tree -m! Error: %w", err)
}
@@ -344,7 +251,7 @@ func AttemptThreeWayMerge(ctx context.Context, gitRepo *git.Repository, base, ou
// Then we use git ls-files -u to list the unmerged files and collate the triples in unmergedfiles
unmerged := make(chan *unmergedFile)
- go unmergedFiles(ctx, gitRepo.Path, unmerged)
+ go unmergedFiles(ctx, gitPath, unmerged)
defer func() {
cancel()
@@ -367,7 +274,7 @@ func AttemptThreeWayMerge(ctx context.Context, gitRepo *git.Repository, base, ou
}
// OK now we have the unmerged file triplet attempt to merge it
- if err := attemptMerge(ctx, file, gitRepo.Path, &filesToRemove, &filesToAdd); err != nil {
+ if err := attemptMerge(ctx, file, gitPath, &filesToRemove, &filesToAdd); err != nil {
if conflictErr, ok := err.(*errMergeConflict); ok {
log.Trace("Conflict: %s in %s", conflictErr.filename, description)
conflict = true
@@ -392,82 +299,14 @@ func AttemptThreeWayMerge(ctx context.Context, gitRepo *git.Repository, base, ou
return conflict, conflictedFiles, nil
}
-// MergeTree runs a 3-way merge between `ours` and `theirs` with
-// `base` as the merge base.
-//
-// It uses git-merge-tree(1) to do this merge without requiring a work-tree and
-// can run in a base repository. It returns the object ID of the merge tree, if
-// there are any conflicts and conflicted files.
-func MergeTree(ctx context.Context, gitRepo *git.Repository, base, ours, theirs string, env []string) (string, bool, []string, error) {
- cmd := git.NewCommand(ctx, "merge-tree", "--write-tree", "-z", "--name-only", "--no-messages")
- if git.CheckGitVersionAtLeast("2.40") == nil {
- cmd.AddOptionFormat("--merge-base=%s", base)
- }
-
- stdout := &bytes.Buffer{}
- gitErr := cmd.AddDynamicArguments(ours, theirs).Run(&git.RunOpts{Dir: gitRepo.Path, Stdout: stdout, Env: env})
- if gitErr != nil && !git.IsErrorExitCode(gitErr, 1) {
- log.Error("Unable to run merge-tree: %v", gitErr)
- return "", false, nil, fmt.Errorf("unable to run merge-tree: %w", gitErr)
- }
-
- // There are two situations that we consider for the output:
- // 1. Clean merge and the output is NUL
- // 2. Merge conflict and the output is NULNUL
- treeOID, conflictedFileInfo, _ := strings.Cut(stdout.String(), "\x00")
- if len(conflictedFileInfo) == 0 {
- return treeOID, git.IsErrorExitCode(gitErr, 1), nil, nil
- }
-
- // Remove last NULL-byte from conflicted file info, then split with NULL byte as seperator.
- return treeOID, true, strings.Split(conflictedFileInfo[:len(conflictedFileInfo)-1], "\x00"), nil
-}
-
-// checkConflicts takes a pull request and checks if merging it would result in
-// merge conflicts and checks if the diff is empty; the status is set accordingly.
-func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, testPatchCtx *testPatchContext) (bool, error) {
- // Resets the conflict status.
+func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository, tmpBasePath string) (bool, error) {
+ // 1. checkConflicts resets the conflict status - therefore - reset the conflict status
pr.ConflictedFiles = nil
- if git.SupportGitMergeTree {
- // Check for conflicts via a merge-tree.
- treeHash, conflict, conflictFiles, err := MergeTree(ctx, testPatchCtx.gitRepo, pr.MergeBase, testPatchCtx.baseRev, testPatchCtx.headRev, testPatchCtx.env)
- if err != nil {
- return false, fmt.Errorf("MergeTree: %w", err)
- }
-
- if !conflict {
- // No conflicts were detected, now check if the pull request actually
- // contains anything useful via a diff. git-diff-tree(1) with --quiet
- // will return exit code 0 if there's no diff and exit code 1 if there's
- // a diff.
- err := git.NewCommand(ctx, "diff-tree", "--quiet").AddDynamicArguments(treeHash, pr.MergeBase).Run(&git.RunOpts{Dir: testPatchCtx.gitRepo.Path, Env: testPatchCtx.env})
- isEmpty := true
- if err != nil {
- if git.IsErrorExitCode(err, 1) {
- isEmpty = false
- } else {
- return false, fmt.Errorf("DiffTree: %w", err)
- }
- }
-
- if isEmpty {
- log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
- pr.Status = issues_model.PullRequestStatusEmpty
- }
- return false, nil
- }
-
- pr.Status = issues_model.PullRequestStatusConflict
- pr.ConflictedFiles = conflictFiles
-
- log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
- return true, nil
- }
-
// 2. AttemptThreeWayMerge first - this is much quicker than plain patch to base
description := fmt.Sprintf("PR[%d] %s/%s#%d", pr.ID, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Index)
- conflict, conflictFiles, err := AttemptThreeWayMerge(ctx, testPatchCtx.gitRepo, pr.MergeBase, testPatchCtx.baseRev, testPatchCtx.headRev, description)
+ conflict, conflictFiles, err := AttemptThreeWayMerge(ctx,
+ tmpBasePath, gitRepo, pr.MergeBase, "base", "tracking", description)
if err != nil {
return false, err
}
@@ -476,13 +315,13 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, testPatch
// No conflicts detected so we need to check if the patch is empty...
// a. Write the newly merged tree and check the new tree-hash
var treeHash string
- treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: testPatchCtx.gitRepo.Path})
+ treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: tmpBasePath})
if err != nil {
- lsfiles, _, _ := git.NewCommand(ctx, "ls-files", "-u").RunStdString(&git.RunOpts{Dir: testPatchCtx.gitRepo.Path})
+ lsfiles, _, _ := git.NewCommand(ctx, "ls-files", "-u").RunStdString(&git.RunOpts{Dir: tmpBasePath})
return false, fmt.Errorf("unable to write unconflicted tree: %w\n`git ls-files -u`:\n%s", err, lsfiles)
}
treeHash = strings.TrimSpace(treeHash)
- baseTree, err := testPatchCtx.gitRepo.GetTree(testPatchCtx.baseRev)
+ baseTree, err := gitRepo.GetTree("base")
if err != nil {
return false, err
}
@@ -496,11 +335,171 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, testPatch
return false, nil
}
- pr.Status = issues_model.PullRequestStatusConflict
- pr.ConflictedFiles = conflictFiles
+ // 3. OK the three-way merge method has detected conflicts
+ // 3a. Are still testing with GitApply? If not set the conflict status and move on
+ if !setting.Repository.PullRequest.TestConflictingPatchesWithGitApply {
+ pr.Status = issues_model.PullRequestStatusConflict
+ pr.ConflictedFiles = conflictFiles
- log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
- return true, nil
+ log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
+ return true, nil
+ }
+
+ // 3b. Create a plain patch from head to base
+ tmpPatchFile, err := os.CreateTemp("", "patch")
+ if err != nil {
+ log.Error("Unable to create temporary patch file! Error: %v", err)
+ return false, fmt.Errorf("unable to create temporary patch file! Error: %w", err)
+ }
+ defer func() {
+ _ = util.Remove(tmpPatchFile.Name())
+ }()
+
+ if err := gitRepo.GetDiffBinary(pr.MergeBase, "tracking", tmpPatchFile); err != nil {
+ tmpPatchFile.Close()
+ log.Error("Unable to get patch file from %s to %s in %s Error: %v", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
+ return false, fmt.Errorf("unable to get patch file from %s to %s in %s Error: %w", pr.MergeBase, pr.HeadBranch, pr.BaseRepo.FullName(), err)
+ }
+ stat, err := tmpPatchFile.Stat()
+ if err != nil {
+ tmpPatchFile.Close()
+ return false, fmt.Errorf("unable to stat patch file: %w", err)
+ }
+ patchPath := tmpPatchFile.Name()
+ tmpPatchFile.Close()
+
+ // 3c. if the size of that patch is 0 - there can be no conflicts!
+ if stat.Size() == 0 {
+ log.Debug("PullRequest[%d]: Patch is empty - ignoring", pr.ID)
+ pr.Status = issues_model.PullRequestStatusEmpty
+ return false, nil
+ }
+
+ log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath)
+
+ // 4. Read the base branch in to the index of the temporary repository
+ _, _, err = git.NewCommand(gitRepo.Ctx, "read-tree", "base").RunStdString(&git.RunOpts{Dir: tmpBasePath})
+ if err != nil {
+ return false, fmt.Errorf("git read-tree %s: %w", pr.BaseBranch, err)
+ }
+
+ // 5. Now get the pull request configuration to check if we need to ignore whitespace
+ prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
+ if err != nil {
+ return false, err
+ }
+ prConfig := prUnit.PullRequestsConfig()
+
+ // 6. Prepare the arguments to apply the patch against the index
+ cmdApply := git.NewCommand(gitRepo.Ctx, "apply", "--check", "--cached")
+ if prConfig.IgnoreWhitespaceConflicts {
+ cmdApply.AddArguments("--ignore-whitespace")
+ }
+ is3way := false
+ if git.CheckGitVersionAtLeast("2.32.0") == nil {
+ cmdApply.AddArguments("--3way")
+ is3way = true
+ }
+ cmdApply.AddDynamicArguments(patchPath)
+
+ // 7. Prep the pipe:
+ // - Here we could do the equivalent of:
+ // `git apply --check --cached patch_file > conflicts`
+ // Then iterate through the conflicts. However, that means storing all the conflicts
+ // in memory - which is very wasteful.
+ // - alternatively we can do the equivalent of:
+ // `git apply --check ... | grep ...`
+ // meaning we don't store all of the conflicts unnecessarily.
+ stderrReader, stderrWriter, err := os.Pipe()
+ if err != nil {
+ log.Error("Unable to open stderr pipe: %v", err)
+ return false, fmt.Errorf("unable to open stderr pipe: %w", err)
+ }
+ defer func() {
+ _ = stderrReader.Close()
+ _ = stderrWriter.Close()
+ }()
+
+ // 8. Run the check command
+ conflict = false
+ err = cmdApply.Run(&git.RunOpts{
+ Dir: tmpBasePath,
+ Stderr: stderrWriter,
+ PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+ // Close the writer end of the pipe to begin processing
+ _ = stderrWriter.Close()
+ defer func() {
+ // Close the reader on return to terminate the git command if necessary
+ _ = stderrReader.Close()
+ }()
+
+ const prefix = "error: patch failed:"
+ const errorPrefix = "error: "
+ const threewayFailed = "Failed to perform three-way merge..."
+ const appliedPatchPrefix = "Applied patch to '"
+ const withConflicts = "' with conflicts."
+
+ conflicts := make(container.Set[string])
+
+ // Now scan the output from the command
+ scanner := bufio.NewScanner(stderrReader)
+ for scanner.Scan() {
+ line := scanner.Text()
+ log.Trace("PullRequest[%d].testPatch: stderr: %s", pr.ID, line)
+ if strings.HasPrefix(line, prefix) {
+ conflict = true
+ filepath := strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0])
+ conflicts.Add(filepath)
+ } else if is3way && line == threewayFailed {
+ conflict = true
+ } else if strings.HasPrefix(line, errorPrefix) {
+ conflict = true
+ for _, suffix := range patchErrorSuffices {
+ if strings.HasSuffix(line, suffix) {
+ filepath := strings.TrimSpace(strings.TrimSuffix(line[len(errorPrefix):], suffix))
+ if filepath != "" {
+ conflicts.Add(filepath)
+ }
+ break
+ }
+ }
+ } else if is3way && strings.HasPrefix(line, appliedPatchPrefix) && strings.HasSuffix(line, withConflicts) {
+ conflict = true
+ filepath := strings.TrimPrefix(strings.TrimSuffix(line, withConflicts), appliedPatchPrefix)
+ if filepath != "" {
+ conflicts.Add(filepath)
+ }
+ }
+ // only list 10 conflicted files
+ if len(conflicts) >= 10 {
+ break
+ }
+ }
+
+ if len(conflicts) > 0 {
+ pr.ConflictedFiles = make([]string, 0, len(conflicts))
+ for key := range conflicts {
+ pr.ConflictedFiles = append(pr.ConflictedFiles, key)
+ }
+ }
+
+ return nil
+ },
+ })
+
+ // 9. Check if the found conflictedfiles is non-zero, "err" could be non-nil, so we should ignore it if we found conflicts.
+ // Note: `"err" could be non-nil` is due that if enable 3-way merge, it doesn't return any error on found conflicts.
+ if len(pr.ConflictedFiles) > 0 {
+ if conflict {
+ pr.Status = issues_model.PullRequestStatusConflict
+ log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
+
+ return true, nil
+ }
+ } else if err != nil {
+ return false, fmt.Errorf("git apply --check: %w", err)
+ }
+ return false, nil
}
// CheckFileProtection check file Protection
@@ -559,7 +558,7 @@ func CheckUnprotectedFiles(repo *git.Repository, oldCommitID, newCommitID string
}
// checkPullFilesProtection check if pr changed protected files and save results
-func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, testPatchCtx *testPatchContext) error {
+func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) error {
if pr.Status == issues_model.PullRequestStatusEmpty {
pr.ChangedProtectedFiles = nil
return nil
@@ -575,7 +574,7 @@ func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest,
return nil
}
- pr.ChangedProtectedFiles, err = CheckFileProtection(testPatchCtx.gitRepo, pr.MergeBase, testPatchCtx.headRev, pb.GetProtectedFilePatterns(), 10, testPatchCtx.env)
+ pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ())
if err != nil && !models.IsErrFilePathProtected(err) {
return err
}
diff --git a/services/pull/patch_test.go b/services/pull/patch_test.go
deleted file mode 100644
index bcab19fc58..0000000000
--- a/services/pull/patch_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-package pull
-
-import (
- "fmt"
- "testing"
-
- issues_model "forgejo.org/models/issues"
- "forgejo.org/models/unittest"
- "forgejo.org/modules/setting"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestLoadHeadRevision(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
-
- t.Run("AGit", func(t *testing.T) {
- t.Run("New", func(t *testing.T) {
- ctx := &testPatchContext{}
- require.NoError(t, ctx.LoadHeadRevision(t.Context(), &issues_model.PullRequest{Flow: issues_model.PullRequestFlowAGit, HeadCommitID: "Commit!"}))
-
- assert.Empty(t, ctx.env)
- assert.Equal(t, "Commit!", ctx.headRev)
- assert.True(t, ctx.headIsCommitID)
- })
- t.Run("Existing", func(t *testing.T) {
- ctx := &testPatchContext{}
- require.NoError(t, ctx.LoadHeadRevision(t.Context(), &issues_model.PullRequest{Flow: issues_model.PullRequestFlowAGit, Index: 371}))
-
- assert.Empty(t, ctx.env)
- assert.Equal(t, "refs/pull/371/head", ctx.headRev)
- assert.False(t, ctx.headIsCommitID)
- })
- })
-
- t.Run("Same repository", func(t *testing.T) {
- ctx := &testPatchContext{}
- require.NoError(t, ctx.LoadHeadRevision(t.Context(), unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})))
-
- assert.Empty(t, ctx.env)
- assert.Equal(t, "refs/heads/branch1", ctx.headRev)
- assert.False(t, ctx.headIsCommitID)
- })
-
- t.Run("Across repository", func(t *testing.T) {
- pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 3})
- require.NoError(t, pr.LoadHeadRepo(t.Context()))
-
- ctx := &testPatchContext{}
- require.NoError(t, ctx.LoadHeadRevision(t.Context(), pr))
-
- if assert.NotEmpty(t, ctx.env) {
- assert.Equal(t, fmt.Sprintf("GIT_ALTERNATE_OBJECT_DIRECTORIES=%s/user13/repo11.git/objects", setting.RepoRootPath), ctx.env[len(ctx.env)-1])
- }
- assert.Equal(t, "0abcb056019adb8336cf9db3ad9d9cf80cd4b141", ctx.headRev)
- assert.True(t, ctx.headIsCommitID)
- })
-}
diff --git a/services/pull/pull.go b/services/pull/pull.go
index c1fe6c6b55..18f63cf95d 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -4,9 +4,11 @@
package pull
import (
+ "bytes"
"context"
"fmt"
"io"
+ "os"
"regexp"
"strings"
"time"
@@ -28,6 +30,7 @@ import (
repo_module "forgejo.org/modules/repository"
"forgejo.org/modules/setting"
"forgejo.org/modules/sync"
+ "forgejo.org/modules/util"
gitea_context "forgejo.org/services/context"
issue_service "forgejo.org/services/issue"
notify_service "forgejo.org/services/notify"
@@ -43,15 +46,22 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
return user_model.ErrBlockedByUser
}
- testPatchCtx, err := testPatch(ctx, pr)
- defer testPatchCtx.close()
+ prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil {
- return fmt.Errorf("testPatch: %w", err)
+ if !git_model.IsErrBranchNotExist(err) {
+ log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
+ }
+ return err
+ }
+ defer cancel()
+
+ if err := testPatch(ctx, prCtx, pr); err != nil {
+ return err
}
- divergence, err := git.GetDivergingCommits(ctx, testPatchCtx.gitRepo.Path, testPatchCtx.baseRev, testPatchCtx.headRev, testPatchCtx.env)
+ divergence, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
if err != nil {
- return fmt.Errorf("GetDivergingCommits: %w", err)
+ return err
}
pr.CommitsAhead = divergence.Ahead
pr.CommitsBehind = divergence.Behind
@@ -381,49 +391,96 @@ func TestPullRequest(ctx context.Context, doer *user_model.User, repoID, olderTh
// Update commit divergence.
func ValidatePullRequest(ctx context.Context, pr *issues_model.PullRequest, newCommitID, oldCommitID string, doer *user_model.User) {
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
- if newCommitID == "" || newCommitID == objectFormat.EmptyObjectID().String() {
- return
- }
-
- testPatchCtx, err := getTestPatchCtx(ctx, pr, true)
- defer testPatchCtx.close()
- if err != nil {
- log.Error("testPatchCtx: %v", err)
- return
- }
-
- changed, err := testPatchCtx.gitRepo.CheckIfDiffDiffers(testPatchCtx.baseRev, oldCommitID, newCommitID, testPatchCtx.env)
- if err != nil {
- log.Error("CheckIfDiffDiffers: %v", err)
- }
- if changed {
- if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
- log.Error("MarkReviewsAsStale: %v", err)
- }
-
- pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+ if newCommitID != "" && newCommitID != objectFormat.EmptyObjectID().String() {
+ changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
if err != nil {
- log.Error("GetFirstMatchProtectedBranchRule: %v", err)
+ log.Error("checkIfPRContentChanged: %v", err)
}
- if pb != nil && pb.DismissStaleApprovals {
- if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
- log.Error("DismissApprovalReviews: %v", err)
+ if changed {
+ if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
+ log.Error("MarkReviewsAsStale: %v", err)
+ }
+
+ pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
+ if err != nil {
+ log.Error("GetFirstMatchProtectedBranchRule: %v", err)
+ }
+ if pb != nil && pb.DismissStaleApprovals {
+ if err := DismissApprovalReviews(ctx, doer, pr); err != nil {
+ log.Error("DismissApprovalReviews: %v", err)
+ }
+ }
+ }
+ if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil {
+ log.Error("MarkReviewsAsNotStale: %v", err)
+ }
+ divergence, err := GetDiverging(ctx, pr)
+ if err != nil {
+ log.Error("GetDiverging: %v", err)
+ } else {
+ err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
+ if err != nil {
+ log.Error("UpdateCommitDivergence: %v", err)
}
}
}
- if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, newCommitID); err != nil {
- log.Error("MarkReviewsAsNotStale: %v", err)
+}
+
+// checkIfPRContentChanged checks if diff to target branch has changed by push
+// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
+func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
+ prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
+ if err != nil {
+ log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
+ return false, err
+ }
+ defer cancel()
+
+ tmpRepo, err := git.OpenRepository(ctx, prCtx.tmpBasePath)
+ if err != nil {
+ return false, fmt.Errorf("OpenRepository: %w", err)
+ }
+ defer tmpRepo.Close()
+
+ // Find the merge-base
+ _, base, err := tmpRepo.GetMergeBase("", "base", "tracking")
+ if err != nil {
+ return false, fmt.Errorf("GetMergeBase: %w", err)
}
- divergence, err := git.GetDivergingCommits(ctx, testPatchCtx.gitRepo.Path, testPatchCtx.baseRev, testPatchCtx.headRev, testPatchCtx.env)
+ cmd := git.NewCommand(ctx, "diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base)
+ stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
- log.Error("GetDivergingCommits: %v", err)
- } else {
- err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
- if err != nil {
- log.Error("UpdateCommitDivergence: %v", err)
- }
+ return false, fmt.Errorf("unable to open pipe for to run diff: %w", err)
}
+
+ stderr := new(bytes.Buffer)
+ if err := cmd.Run(&git.RunOpts{
+ Dir: prCtx.tmpBasePath,
+ Stdout: stdoutWriter,
+ Stderr: stderr,
+ PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
+ _ = stdoutWriter.Close()
+ defer func() {
+ _ = stdoutReader.Close()
+ }()
+ return util.IsEmptyReader(stdoutReader)
+ },
+ }); err != nil {
+ if err == util.ErrNotEmpty {
+ return true, nil
+ }
+ err = git.ConcatenateError(err, stderr.String())
+
+ log.Error("Unable to run diff on %s %s %s in tempRepo for PR[%d]%s/%s...%s/%s: Error: %v",
+ newCommitID, oldCommitID, base,
+ pr.ID, pr.BaseRepo.FullName(), pr.BaseBranch, pr.HeadRepo.FullName(), pr.HeadBranch,
+ err)
+
+ return false, fmt.Errorf("Unable to run git diff --name-only -z %s %s %s: %w", newCommitID, oldCommitID, base, err)
+ }
+
+ return false, nil
}
// PushToBaseRepo pushes commits from branches of head repository to
diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go
index 99607c5b35..010b7a6404 100644
--- a/services/pull/pull_test.go
+++ b/services/pull/pull_test.go
@@ -92,39 +92,3 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage)
}
-
-func TestPullRequest_GetDefaultMergeMessage_GlobalTemplate(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
-
- require.NoError(t, pr.LoadBaseRepo(t.Context()))
- gitRepo, err := gitrepo.OpenRepository(t.Context(), pr.BaseRepo)
- require.NoError(t, err)
- defer gitRepo.Close()
-
- templateRepo, err := git.OpenRepository(t.Context(), "./../../modules/git/tests/repos/templates_repo")
- require.NoError(t, err)
- defer templateRepo.Close()
-
- mergeMessageTemplates[repo_model.MergeStyleMerge] = "${PullRequestTitle} (${PullRequestReference})\n${PullRequestDescription}"
-
- // Check template is used for Merge...
- mergeMessage, body, err := GetDefaultMergeMessage(t.Context(), gitRepo, pr, repo_model.MergeStyleMerge)
- require.NoError(t, err)
-
- assert.Equal(t, "issue3 (#3)", mergeMessage)
- assert.Equal(t, "content for the third issue", body)
-
- // ...but not for RebaseMerge
- mergeMessage, _, err = GetDefaultMergeMessage(t.Context(), gitRepo, pr, repo_model.MergeStyleRebaseMerge)
- require.NoError(t, err)
-
- assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", mergeMessage)
-
- // ...and that custom Merge template takes priority
- mergeMessage, body, err = GetDefaultMergeMessage(t.Context(), templateRepo, pr, repo_model.MergeStyleMerge)
- require.NoError(t, err)
-
- assert.Equal(t, "Default merge message template", mergeMessage)
- assert.Equal(t, "This line was read from .forgejo/default_merge_message/MERGE_TEMPLATE.md", body)
-}
diff --git a/services/pull/review.go b/services/pull/review.go
index b0ab700fa6..d61d9623b6 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -6,9 +6,10 @@ package pull
import (
"context"
- "errors"
"fmt"
"io"
+ "regexp"
+ "strings"
"forgejo.org/models/db"
issues_model "forgejo.org/models/issues"
@@ -23,6 +24,8 @@ import (
notify_service "forgejo.org/services/notify"
)
+var notEnoughLines = regexp.MustCompile(`fatal: file .* has only \d+ lines?`)
+
// ErrDismissRequestOnClosedPR represents an error when an user tries to dismiss a review associated to a closed or merged PR.
type ErrDismissRequestOnClosedPR struct{}
@@ -44,8 +47,8 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
// If the line got changed the comment is going to be invalidated.
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *git.Repository, branch string) error {
// FIXME differentiate between previous and proposed line
- commit, err := repo.LineBlame(branch, c.TreePath, c.UnsignedLine())
- if err != nil && (errors.Is(err, git.ErrBlameFileDoesNotExist) || errors.Is(err, git.ErrBlameFileNotEnoughLines)) {
+ commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
+ if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
c.Invalidated = true
return issues_model.UpdateCommentInvalidate(ctx, c)
}
@@ -226,10 +229,10 @@ func CreateCodeCommentKnownReviewID(ctx context.Context, doer *user_model.User,
// FIXME validate treePath
// Get latest commit referencing the commented line
// No need for get commit for base branch changes
- commit, err := gitRepo.LineBlame(head, treePath, uint64(line))
+ commit, err := gitRepo.LineBlame(head, gitRepo.Path, treePath, uint(line))
if err == nil {
commitID = commit.ID.String()
- } else if !errors.Is(err, git.ErrBlameFileDoesNotExist) && !errors.Is(err, git.ErrBlameFileNotEnoughLines) {
+ } else if !(strings.Contains(err.Error(), "exit status 128 - fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitRefName(), gitRepo.Path, treePath, line, err)
}
}
@@ -298,16 +301,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
if headCommitID == commitID {
stale = false
} else {
- testPatchCtx, err := getTestPatchCtx(ctx, pr, true)
- defer testPatchCtx.close()
+ stale, err = checkIfPRContentChanged(ctx, pr, commitID, headCommitID)
if err != nil {
return nil, nil, err
}
-
- stale, err = testPatchCtx.gitRepo.CheckIfDiffDiffers(testPatchCtx.baseRev, commitID, headCommitID, testPatchCtx.env)
- if err != nil {
- return nil, nil, fmt.Errorf("CheckIfDiffDiffers: %w", err)
- }
}
}
@@ -390,7 +387,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
}
if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject {
- return nil, errors.New("not need to dismiss this review because it's type is not Approve or change request")
+ return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request")
}
// load data for notify
@@ -400,7 +397,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
// Check if the review's repoID is the one we're currently expecting.
if review.Issue.RepoID != repoID {
- return nil, errors.New("reviews's repository is not the same as the one we expect")
+ return nil, fmt.Errorf("reviews's repository is not the same as the one we expect")
}
issue := review.Issue
diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go
index 76ae0df018..1805ffc527 100644
--- a/services/pull/temp_repo.go
+++ b/services/pull/temp_repo.go
@@ -103,7 +103,11 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest)
remoteRepoName := "head_repo"
baseBranch := "base"
- fetchArgs := git.TrustedCmdArgs{"--no-tags", "--no-write-commit-graph"}
+ fetchArgs := git.TrustedCmdArgs{"--no-tags"}
+ if git.CheckGitVersionAtLeast("2.25.0") == nil {
+ // Writing the commit graph can be slow and is not needed here
+ fetchArgs = append(fetchArgs, "--no-write-commit-graph")
+ }
// addCacheRepo adds git alternatives for the cacheRepoPath in the repoPath
addCacheRepo := func(repoPath, cacheRepoPath string) error {
diff --git a/services/pull/update.go b/services/pull/update.go
index 563c11fff3..1b4b6b039d 100644
--- a/services/pull/update.go
+++ b/services/pull/update.go
@@ -5,7 +5,6 @@ package pull
import (
"context"
- "errors"
"fmt"
git_model "forgejo.org/models/git"
@@ -23,7 +22,7 @@ import (
func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
if pr.Flow == issues_model.PullRequestFlowAGit {
// TODO: update of agit flow pull request's head branch is unsupported
- return errors.New("update of agit flow pull request's head branch is unsupported")
+ return fmt.Errorf("update of agit flow pull request's head branch is unsupported")
}
pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
@@ -176,6 +175,6 @@ func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.Diver
}
defer cancel()
- diff, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch, nil)
+ diff, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
return &diff, err
}
diff --git a/services/redirect/main_test.go b/services/redirect/main_test.go
deleted file mode 100644
index 7363791caa..0000000000
--- a/services/redirect/main_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-package redirect
-
-import (
- "testing"
-
- "forgejo.org/models/unittest"
-
- _ "forgejo.org/models"
- _ "forgejo.org/models/actions"
- _ "forgejo.org/models/activities"
- _ "forgejo.org/models/forgefed"
-)
-
-func TestMain(m *testing.M) {
- unittest.MainTest(m)
-}
diff --git a/services/redirect/repo.go b/services/redirect/repo.go
deleted file mode 100644
index 7070ab4e2f..0000000000
--- a/services/redirect/repo.go
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-package redirect
-
-import (
- "context"
-
- access_model "forgejo.org/models/perm/access"
- repo_model "forgejo.org/models/repo"
- user_model "forgejo.org/models/user"
-)
-
-// LookupRepoRedirect returns the repository ID if there's a redirect registered for
-// the ownerID repository name pair. It checks if the doer has permission to view
-// the new repository.
-func LookupRepoRedirect(ctx context.Context, doer *user_model.User, ownerID int64, repoName string) (int64, error) {
- redirectID, err := repo_model.GetRedirect(ctx, ownerID, repoName)
- if err != nil {
- return 0, err
- }
-
- redirectRepo, err := repo_model.GetRepositoryByID(ctx, redirectID)
- if err != nil {
- return 0, err
- }
-
- perm, err := access_model.GetUserRepoPermission(ctx, redirectRepo, doer)
- if err != nil {
- return 0, err
- }
-
- if !perm.HasAccess() {
- return 0, repo_model.ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName, MissingPermission: true}
- }
-
- return redirectID, nil
-}
diff --git a/services/redirect/repo_test.go b/services/redirect/repo_test.go
deleted file mode 100644
index cad8414035..0000000000
--- a/services/redirect/repo_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-package redirect
-
-import (
- "testing"
-
- repo_model "forgejo.org/models/repo"
- "forgejo.org/models/unittest"
- user_model "forgejo.org/models/user"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestLookupRepoRedirect(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
-
- normalUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
- ownerUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 20})
-
- testOk := func(t *testing.T, doer *user_model.User, ownerID int64, repoName string, expectedRedirectID int64) {
- t.Helper()
-
- redirectID, err := LookupRepoRedirect(t.Context(), doer, ownerID, repoName)
- require.NoError(t, err)
- assert.Equal(t, expectedRedirectID, redirectID)
- }
-
- testFail := func(t *testing.T, doer *user_model.User, ownerID int64, repoName string) {
- t.Helper()
-
- redirectID, err := LookupRepoRedirect(t.Context(), doer, ownerID, repoName)
- require.ErrorIs(t, err, repo_model.ErrRedirectNotExist{OwnerID: ownerID, RepoName: repoName, MissingPermission: true})
- assert.Zero(t, redirectID)
- }
-
- t.Run("Public repository", func(t *testing.T) {
- ownerID := int64(2)
- reponame := "oldrepo1"
-
- testOk(t, nil, ownerID, reponame, 1)
- testOk(t, normalUser, ownerID, reponame, 1)
- testOk(t, ownerUser, ownerID, reponame, 1)
- })
-
- t.Run("Private repository", func(t *testing.T) {
- ownerID := int64(17)
- reponame := "oldrepo24"
-
- testFail(t, nil, ownerID, reponame)
- testFail(t, normalUser, ownerID, reponame)
- testOk(t, ownerUser, ownerID, reponame, 24)
- })
-}
diff --git a/services/redirect/user.go b/services/redirect/user.go
deleted file mode 100644
index 2b1d38bbc0..0000000000
--- a/services/redirect/user.go
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-package redirect
-
-import (
- "context"
-
- user_model "forgejo.org/models/user"
-)
-
-// LookupUserRedirect returns the userID if there's a redirect registered for the
-// username. It additionally checks if the doer has permission to view the new
-// user.
-func LookupUserRedirect(ctx context.Context, doer *user_model.User, userName string) (int64, error) {
- redirect, err := user_model.GetUserRedirect(ctx, userName)
- if err != nil {
- return 0, err
- }
-
- redirectUser, err := user_model.GetUserByID(ctx, redirect.RedirectUserID)
- if err != nil {
- return 0, err
- }
-
- if !user_model.IsUserVisibleToViewer(ctx, redirectUser, doer) {
- return 0, user_model.ErrUserRedirectNotExist{Name: userName, MissingPermission: true}
- }
-
- return redirect.RedirectUserID, nil
-}
diff --git a/services/redirect/user_test.go b/services/redirect/user_test.go
deleted file mode 100644
index d1ddcc2ebf..0000000000
--- a/services/redirect/user_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-package redirect
-
-import (
- "testing"
-
- "forgejo.org/models/unittest"
- user_model "forgejo.org/models/user"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestLookupUserRedirect(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
-
- adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- normalUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-
- testOk := func(t *testing.T, doer *user_model.User, username string, expectedRedirectID int64) {
- t.Helper()
-
- redirectID, err := LookupUserRedirect(t.Context(), doer, username)
- require.NoError(t, err)
- assert.Equal(t, expectedRedirectID, redirectID)
- }
-
- testFail := func(t *testing.T, doer *user_model.User, username string) {
- t.Helper()
-
- redirectID, err := LookupUserRedirect(t.Context(), doer, username)
- require.ErrorIs(t, err, user_model.ErrUserRedirectNotExist{Name: username, MissingPermission: true})
- assert.Zero(t, redirectID)
- }
-
- t.Run("Public visibility", func(t *testing.T) {
- username := "olduser1"
- redirectID := int64(1)
-
- testOk(t, nil, username, redirectID)
- testOk(t, normalUser, username, redirectID)
- testOk(t, adminUser, username, redirectID)
- })
-
- t.Run("Limited visibility", func(t *testing.T) {
- username := "oldorg22"
- redirectID := int64(22)
-
- testFail(t, nil, username)
- testOk(t, normalUser, username, redirectID)
- testOk(t, adminUser, username, redirectID)
- })
-
- t.Run("Private visibility", func(t *testing.T) {
- orgUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
- username := "oldorg23"
- redirectID := int64(23)
-
- testFail(t, nil, username)
- testFail(t, normalUser, username)
- testOk(t, orgUser, username, redirectID)
- testOk(t, adminUser, username, redirectID)
- })
-}
diff --git a/services/release/release.go b/services/release/release.go
index 90eb1320ed..f0682c4dca 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -161,17 +161,17 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, msg string,
for _, attachmentChange := range attachmentChanges {
if attachmentChange.Action != "add" {
- return errors.New("can only create new attachments when creating release")
+ return fmt.Errorf("can only create new attachments when creating release")
}
switch attachmentChange.Type {
case "attachment":
if attachmentChange.UUID == "" {
- return errors.New("new attachment should have a uuid")
+ return fmt.Errorf("new attachment should have a uuid")
}
addAttachmentUUIDs.Add(attachmentChange.UUID)
case "external":
if attachmentChange.Name == "" || attachmentChange.ExternalURL == "" {
- return errors.New("new external attachment should have a name and external url")
+ return fmt.Errorf("new external attachment should have a name and external url")
}
_, err = attachment.NewExternalAttachment(gitRepo.Ctx, &repo_model.Attachment{
@@ -186,7 +186,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, msg string,
}
default:
if attachmentChange.Type == "" {
- return errors.New("missing attachment type")
+ return fmt.Errorf("missing attachment type")
}
return fmt.Errorf("unknown attachment type: '%q'", attachmentChange.Type)
}
@@ -280,7 +280,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
addAttachmentUUIDs.Add(attachmentChange.UUID)
case "external":
if attachmentChange.Name == "" || attachmentChange.ExternalURL == "" {
- return errors.New("new external attachment should have a name and external url")
+ return fmt.Errorf("new external attachment should have a name and external url")
}
_, err := attachment.NewExternalAttachment(ctx, &repo_model.Attachment{
Name: attachmentChange.Name,
@@ -294,13 +294,13 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
}
default:
if attachmentChange.Type == "" {
- return errors.New("missing attachment type")
+ return fmt.Errorf("missing attachment type")
}
return fmt.Errorf("unknown attachment type: %q", attachmentChange.Type)
}
case "delete":
if attachmentChange.UUID == "" {
- return errors.New("attachment deletion should have a uuid")
+ return fmt.Errorf("attachment deletion should have a uuid")
}
delAttachmentUUIDs.Add(attachmentChange.UUID)
case "update":
@@ -308,7 +308,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
updateAttachments.Add(attachmentChange)
default:
if attachmentChange.Action == "" {
- return errors.New("missing attachment action")
+ return fmt.Errorf("missing attachment action")
}
return fmt.Errorf("unknown attachment action: %q", attachmentChange.Action)
}
diff --git a/services/release/release_test.go b/services/release/release_test.go
index f03b4d42b8..66106eb606 100644
--- a/services/release/release_test.go
+++ b/services/release/release_test.go
@@ -6,6 +6,7 @@ package release
import (
"strings"
"testing"
+ "time"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
@@ -13,7 +14,6 @@ import (
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
- "forgejo.org/modules/test"
"forgejo.org/services/attachment"
_ "forgejo.org/models/actions"
@@ -138,9 +138,9 @@ func TestRelease_Create(t *testing.T) {
}))
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, &release))
assert.Len(t, release.Attachments, 1)
- assert.Equal(t, attach.UUID, release.Attachments[0].UUID)
- assert.Equal(t, attach.Name, release.Attachments[0].Name)
- assert.Equal(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
+ assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
+ assert.EqualValues(t, attach.Name, release.Attachments[0].Name)
+ assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
release = repo_model.Release{
RepoID: repo.ID,
@@ -165,8 +165,8 @@ func TestRelease_Create(t *testing.T) {
}))
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, &release))
assert.Len(t, release.Attachments, 1)
- assert.Equal(t, "test", release.Attachments[0].Name)
- assert.Equal(t, "https://forgejo.org/", release.Attachments[0].ExternalURL)
+ assert.EqualValues(t, "test", release.Attachments[0].Name)
+ assert.EqualValues(t, "https://forgejo.org/", release.Attachments[0].ExternalURL)
release = repo_model.Release{
RepoID: repo.ID,
@@ -219,7 +219,7 @@ func TestRelease_Update(t *testing.T) {
release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.1.1")
require.NoError(t, err)
releaseCreatedUnix := release.CreatedUnix
- test.SleepTillNextSecond()
+ time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Note = "Changed note"
require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{}))
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
@@ -243,7 +243,7 @@ func TestRelease_Update(t *testing.T) {
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.2.1")
require.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
- test.SleepTillNextSecond()
+ time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{}))
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
@@ -267,7 +267,7 @@ func TestRelease_Update(t *testing.T) {
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.3.1")
require.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
- test.SleepTillNextSecond()
+ time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
release.Note = "Changed note"
require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{}))
@@ -318,10 +318,10 @@ func TestRelease_Update(t *testing.T) {
}))
require.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
assert.Len(t, release.Attachments, 1)
- assert.Equal(t, attach.UUID, release.Attachments[0].UUID)
- assert.Equal(t, release.ID, release.Attachments[0].ReleaseID)
- assert.Equal(t, attach.Name, release.Attachments[0].Name)
- assert.Equal(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
+ assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
+ assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
+ assert.EqualValues(t, attach.Name, release.Attachments[0].Name)
+ assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
// update the attachment name
require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
@@ -334,10 +334,10 @@ func TestRelease_Update(t *testing.T) {
release.Attachments = nil
require.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
assert.Len(t, release.Attachments, 1)
- assert.Equal(t, attach.UUID, release.Attachments[0].UUID)
- assert.Equal(t, release.ID, release.Attachments[0].ReleaseID)
- assert.Equal(t, "test2.txt", release.Attachments[0].Name)
- assert.Equal(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
+ assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
+ assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
+ assert.EqualValues(t, "test2.txt", release.Attachments[0].Name)
+ assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
// delete the attachment
require.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
@@ -361,9 +361,9 @@ func TestRelease_Update(t *testing.T) {
}))
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
assert.Len(t, release.Attachments, 1)
- assert.Equal(t, release.ID, release.Attachments[0].ReleaseID)
- assert.Equal(t, "test", release.Attachments[0].Name)
- assert.Equal(t, "https://forgejo.org/", release.Attachments[0].ExternalURL)
+ assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
+ assert.EqualValues(t, "test", release.Attachments[0].Name)
+ assert.EqualValues(t, "https://forgejo.org/", release.Attachments[0].ExternalURL)
externalAttachmentUUID := release.Attachments[0].UUID
// update the attachment name
@@ -378,10 +378,10 @@ func TestRelease_Update(t *testing.T) {
release.Attachments = nil
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
assert.Len(t, release.Attachments, 1)
- assert.Equal(t, externalAttachmentUUID, release.Attachments[0].UUID)
- assert.Equal(t, release.ID, release.Attachments[0].ReleaseID)
- assert.Equal(t, "test2", release.Attachments[0].Name)
- assert.Equal(t, "https://about.gitea.com/", release.Attachments[0].ExternalURL)
+ assert.EqualValues(t, externalAttachmentUUID, release.Attachments[0].UUID)
+ assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
+ assert.EqualValues(t, "test2", release.Attachments[0].Name)
+ assert.EqualValues(t, "https://about.gitea.com/", release.Attachments[0].ExternalURL)
}
func TestRelease_createTag(t *testing.T) {
@@ -412,7 +412,7 @@ func TestRelease_createTag(t *testing.T) {
require.NoError(t, err)
assert.NotEmpty(t, release.CreatedUnix)
releaseCreatedUnix := release.CreatedUnix
- test.SleepTillNextSecond()
+ time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Note = "Changed note"
_, err = createTag(db.DefaultContext, gitRepo, release, "")
require.NoError(t, err)
@@ -435,7 +435,7 @@ func TestRelease_createTag(t *testing.T) {
_, err = createTag(db.DefaultContext, gitRepo, release, "")
require.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
- test.SleepTillNextSecond()
+ time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
_, err = createTag(db.DefaultContext, gitRepo, release, "")
require.NoError(t, err)
@@ -458,7 +458,7 @@ func TestRelease_createTag(t *testing.T) {
_, err = createTag(db.DefaultContext, gitRepo, release, "")
require.NoError(t, err)
releaseCreatedUnix = release.CreatedUnix
- test.SleepTillNextSecond()
+ time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
release.Title = "Changed title"
release.Note = "Changed note"
_, err = createTag(db.DefaultContext, gitRepo, release, "")
diff --git a/services/remote/promote.go b/services/remote/promote.go
index f37d00168c..26a54fb635 100644
--- a/services/remote/promote.go
+++ b/services/remote/promote.go
@@ -98,7 +98,7 @@ func getRemoteUserToPromote(ctx context.Context, source *auth_model.Source, logi
return nil, NewReason(log.ERROR, ReasonErrorLoginName, "getUserByLoginName('%s') %v", loginName, err), err
}
if len(users) == 0 {
- return nil, NewReason(log.DEBUG, ReasonLoginNameNotExists, "no user with LoginType UserTypeRemoteUser and LoginName '%s'", loginName), nil
+ return nil, NewReason(log.ERROR, ReasonLoginNameNotExists, "no user with LoginType UserTypeRemoteUser and LoginName '%s'", loginName), nil
}
reason := ReasonNoSource
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index 00d82267c9..ec4da2404f 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -36,7 +36,7 @@ func TestArchive_Basic(t *testing.T) {
bogusReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP)
require.NoError(t, err)
assert.NotNil(t, bogusReq)
- assert.Equal(t, firstCommit+".zip", bogusReq.GetArchiveName())
+ assert.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName())
// Check a series of bogus requests.
// Step 1, valid commit with a bad extension.
@@ -57,12 +57,12 @@ func TestArchive_Basic(t *testing.T) {
bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "master", git.ZIP)
require.NoError(t, err)
assert.NotNil(t, bogusReq)
- assert.Equal(t, "master.zip", bogusReq.GetArchiveName())
+ assert.EqualValues(t, "master.zip", bogusReq.GetArchiveName())
bogusReq, err = NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, "test/archive", git.ZIP)
require.NoError(t, err)
assert.NotNil(t, bogusReq)
- assert.Equal(t, "test-archive.zip", bogusReq.GetArchiveName())
+ assert.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName())
// Now two valid requests, firstCommit with valid extensions.
zipReq, err := NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, firstCommit, git.ZIP)
diff --git a/services/repository/avatar_test.go b/services/repository/avatar_test.go
index 6f28113286..e5fcf7f239 100644
--- a/services/repository/avatar_test.go
+++ b/services/repository/avatar_test.go
@@ -60,7 +60,7 @@ func TestDeleteAvatar(t *testing.T) {
err = DeleteAvatar(db.DefaultContext, repo)
require.NoError(t, err)
- assert.Empty(t, repo.Avatar)
+ assert.Equal(t, "", repo.Avatar)
}
func TestTemplateGenerateAvatar(t *testing.T) {
diff --git a/services/repository/branch.go b/services/repository/branch.go
index bc739825a5..689f35803d 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -28,7 +28,6 @@ import (
"forgejo.org/modules/timeutil"
"forgejo.org/modules/util"
webhook_module "forgejo.org/modules/webhook"
- actions_service "forgejo.org/services/actions"
notify_service "forgejo.org/services/notify"
pull_service "forgejo.org/services/pull"
files_service "forgejo.org/services/repository/files"
@@ -251,7 +250,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
// For other batches, it will hit optimization 4.
if len(branchNames) != len(commitIDs) {
- return errors.New("branchNames and commitIDs length not match")
+ return fmt.Errorf("branchNames and commitIDs length not match")
}
return db.WithTx(ctx, func(ctx context.Context) error {
@@ -378,7 +377,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
log.Error("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
- if err := actions_service.CancelPreviousJobs(
+ if err := actions_model.CancelPreviousJobs(
ctx,
repo.ID,
from,
@@ -579,7 +578,7 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR
log.Error("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
- if err := actions_service.CancelPreviousJobs(
+ if err := actions_model.CancelPreviousJobs(
ctx,
repo.ID,
oldDefaultBranchName,
diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
index 1805bd5960..ad4cc400cb 100644
--- a/services/repository/contributors_graph.go
+++ b/services/repository/contributors_graph.go
@@ -111,7 +111,7 @@ func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_mode
var cachedStats map[string]*ContributorData
return cachedStats, json.Unmarshal([]byte(v), &cachedStats)
default:
- return nil, errors.New("unexpected type in cache detected")
+ return nil, fmt.Errorf("unexpected type in cache detected")
}
}
diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go
index 45af85272d..927c950bec 100644
--- a/services/repository/contributors_graph_test.go
+++ b/services/repository/contributors_graph_test.go
@@ -53,14 +53,14 @@ func TestRepository_ContributorsGraph(t *testing.T) {
keys = append(keys, k)
}
slices.Sort(keys)
- assert.Equal(t, []string{
+ assert.EqualValues(t, []string{
"ethantkoenig@gmail.com",
"jimmy.praet@telenet.be",
"jon@allspice.io",
"total", // generated summary
}, keys)
- assert.Equal(t, &ContributorData{
+ assert.EqualValues(t, &ContributorData{
Name: "Ethan Koenig",
AvatarLink: "/assets/img/avatar_default.png",
TotalCommits: 1,
@@ -73,7 +73,7 @@ func TestRepository_ContributorsGraph(t *testing.T) {
},
},
}, data["ethantkoenig@gmail.com"])
- assert.Equal(t, &ContributorData{
+ assert.EqualValues(t, &ContributorData{
Name: "Total",
AvatarLink: "",
TotalCommits: 3,
diff --git a/services/repository/create_test.go b/services/repository/create_test.go
index 0a6c34b6fe..7eb3c0f805 100644
--- a/services/repository/create_test.go
+++ b/services/repository/create_test.go
@@ -147,13 +147,3 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
}
require.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
}
-
-func TestCreateRepository(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
-
- r, err := CreateRepositoryDirectly(db.DefaultContext, user, user, CreateRepoOptions{Name: "repo-last"})
- require.NoError(t, err)
- require.NotNil(t, r.Topics)
- require.Empty(t, r.Topics)
-}
diff --git a/services/repository/delete.go b/services/repository/delete.go
index f4124fb9e2..7c83ba12cd 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -1,5 +1,4 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
-// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
@@ -90,11 +89,6 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
}
}
- // If the repository was reported as abusive, a shadow copy should be created before deletion.
- if err := repo_model.IfNeededCreateShadowCopyForRepository(ctx, repo, false); err != nil {
- return err
- }
-
if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil {
return err
} else if cnt != 1 {
diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go
index 0e88a29230..b6d54c4086 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -5,7 +5,6 @@ package files
import (
"context"
- "errors"
"fmt"
"strings"
@@ -80,33 +79,21 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
right, base = base, right
}
- var treeHash string
- if git.SupportGitMergeTree {
- var conflict bool
- treeHash, conflict, _, err = pull.MergeTree(ctx, t.gitRepo, base, opts.LastCommitID, right, nil)
- if err != nil {
- return nil, fmt.Errorf("failed to three-way merge %s onto %s: %w", right, opts.OldBranch, err)
- }
+ description := fmt.Sprintf("CherryPick %s onto %s", right, opts.OldBranch)
+ conflict, _, err := pull.AttemptThreeWayMerge(ctx,
+ t.basePath, t.gitRepo, base, opts.LastCommitID, right, description)
+ if err != nil {
+ return nil, fmt.Errorf("failed to three-way merge %s onto %s: %w", right, opts.OldBranch, err)
+ }
- if conflict {
- return nil, errors.New("failed to merge due to conflicts")
- }
- } else {
- description := fmt.Sprintf("CherryPick %s onto %s", right, opts.OldBranch)
- conflict, _, err := pull.AttemptThreeWayMerge(ctx, t.gitRepo, base, opts.LastCommitID, right, description)
- if err != nil {
- return nil, fmt.Errorf("failed to three-way merge %s onto %s: %w", right, opts.OldBranch, err)
- }
+ if conflict {
+ return nil, fmt.Errorf("failed to merge due to conflicts")
+ }
- if conflict {
- return nil, errors.New("failed to merge due to conflicts")
- }
-
- treeHash, err = t.WriteTree()
- if err != nil {
- // likely non-sensical tree due to merge conflicts...
- return nil, err
- }
+ treeHash, err := t.WriteTree()
+ if err != nil {
+ // likely non-sensical tree due to merge conflicts...
+ return nil, err
}
// Now commit the tree
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index 43c9048aaf..0c0671429b 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -1,5 +1,4 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
-// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package files
@@ -15,7 +14,7 @@ import (
// CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch
func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, branch string) (*git.DivergeObject, error) {
- divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch, nil)
+ divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch)
if err != nil {
return nil, err
}
@@ -39,7 +38,7 @@ func GetPayloadCommitVerification(ctx context.Context, commit *git.Commit) *stru
verification.Verified = commitVerification.Verified
verification.Reason = commitVerification.Reason
if verification.Reason == "" && !verification.Verified {
- verification.Reason = asymkey_model.NotSigned
+ verification.Reason = "gpg.error.not_signed_commit"
}
return verification
}
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index d701508ff0..3eb3049f12 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -5,7 +5,6 @@ package files
import (
"context"
- "errors"
"fmt"
"net/url"
"path"
@@ -108,7 +107,7 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
switch {
case entry.IsDir():
return ContentTypeDir
- case entry.IsSubmodule():
+ case entry.IsSubModule():
return ContentTypeSubmodule
case entry.IsExecutable(), entry.IsRegular():
return ContentTypeRegular
@@ -179,13 +178,12 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
// All content types have these fields in populated
contentsResponse := &api.ContentsResponse{
- Name: entry.Name(),
- Path: treePath,
- SHA: entry.ID.String(),
- LastCommitSHA: lastCommit.ID.String(),
- LastCommitWhen: lastCommit.Committer.When,
- Size: entry.Size(),
- URL: &selfURLString,
+ Name: entry.Name(),
+ Path: treePath,
+ SHA: entry.ID.String(),
+ LastCommitSHA: lastCommit.ID.String(),
+ Size: entry.Size(),
+ URL: &selfURLString,
Links: &api.FileLinksResponse{
Self: &selfURLString,
},
@@ -206,19 +204,19 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
} else if entry.IsLink() {
contentsResponse.Type = string(ContentTypeLink)
// The target of a symlink file is the content of the file
- targetFromContent, err := entry.LinkTarget()
+ targetFromContent, err := entry.Blob().GetBlobContent(1024)
if err != nil {
return nil, err
}
contentsResponse.Target = &targetFromContent
- } else if entry.IsSubmodule() {
+ } else if entry.IsSubModule() {
contentsResponse.Type = string(ContentTypeSubmodule)
- submodule, err := commit.GetSubmodule(treePath, entry)
+ submoduleURL, err := commit.GetSubModule(treePath)
if err != nil {
return nil, err
}
- if submodule.URL != "" {
- contentsResponse.SubmoduleGitURL = &submodule.URL
+ if submoduleURL != "" {
+ contentsResponse.SubmoduleGitURL = &submoduleURL
}
}
// Handle links
@@ -230,7 +228,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
downloadURLString := downloadURL.String()
contentsResponse.DownloadURL = &downloadURLString
}
- if !entry.IsSubmodule() {
+ if !entry.IsSubModule() {
htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
if err != nil {
return nil, err
@@ -251,35 +249,20 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
return contentsResponse, nil
}
-// GetBlobsBySHA gets multiple GitBlobs of a repository by sha hash.
-func GetBlobsBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, shas []string) ([]*api.GitBlob, error) {
- if len(shas) > setting.API.MaxResponseItems {
- shas = shas[:setting.API.MaxResponseItems]
- }
-
- blobs := make([]*api.GitBlob, 0, len(shas))
- for _, sha := range shas {
- blob, err := GetBlobBySHA(ctx, repo, gitRepo, sha)
- if err != nil {
- return nil, err
- }
- blobs = append(blobs, blob)
- }
- return blobs, nil
-}
-
-// GetBlobBySHA get the GitBlob of a repository using a sha hash.
-func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlob, error) {
+// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash.
+func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
gitBlob, err := gitRepo.GetBlob(sha)
if err != nil {
return nil, err
}
- content, err := gitBlob.GetContentBase64(setting.API.DefaultMaxBlobSize)
- if err != nil && !errors.As(err, &git.BlobTooLargeError{}) {
- return nil, err
+ content := ""
+ if gitBlob.Size() <= setting.API.DefaultMaxBlobSize {
+ content, err = gitBlob.GetBlobContentBase64()
+ if err != nil {
+ return nil, err
+ }
}
-
- return &api.GitBlob{
+ return &api.GitBlobResponse{
SHA: gitBlob.ID.String(),
URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
Size: gitBlob.Size(),
diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go
index 8fc8f56b4f..ca2f861c0b 100644
--- a/services/repository/files/content_test.go
+++ b/services/repository/files/content_test.go
@@ -5,7 +5,6 @@ package files
import (
"testing"
- "time"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
@@ -34,19 +33,18 @@ func getExpectedReadmeContentsResponse() *api.ContentsResponse {
gitURL := "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/" + sha
downloadURL := "https://try.gitea.io/user2/repo1/raw/branch/master/" + treePath
return &api.ContentsResponse{
- Name: treePath,
- Path: treePath,
- SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
- LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
- LastCommitWhen: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)),
- Type: "file",
- Size: 30,
- Encoding: &encoding,
- Content: &content,
- URL: &selfURL,
- HTMLURL: &htmlURL,
- GitURL: &gitURL,
- DownloadURL: &downloadURL,
+ Name: treePath,
+ Path: treePath,
+ SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
+ LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
+ Type: "file",
+ Size: 30,
+ Encoding: &encoding,
+ Content: &content,
+ URL: &selfURL,
+ HTMLURL: &htmlURL,
+ GitURL: &gitURL,
+ DownloadURL: &downloadURL,
Links: &api.FileLinksResponse{
Self: &selfURL,
GitURL: &gitURL,
@@ -66,13 +64,13 @@ func TestGetContents(t *testing.T) {
t.Run("Get README.md contents with GetContents(ctx, )", func(t *testing.T) {
fileContentResponse, err := GetContents(db.DefaultContext, repo, treePath, ref, false)
- assert.Equal(t, expectedContentsResponse, fileContentResponse)
+ assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
require.NoError(t, err)
})
t.Run("Get README.md contents with ref as empty string (should then use the repo's default branch) with GetContents(ctx, )", func(t *testing.T) {
fileContentResponse, err := GetContents(db.DefaultContext, repo, treePath, "", false)
- assert.Equal(t, expectedContentsResponse, fileContentResponse)
+ assert.EqualValues(t, expectedContentsResponse, fileContentResponse)
require.NoError(t, err)
})
}
@@ -192,7 +190,7 @@ func TestGetBlobBySHA(t *testing.T) {
defer gitRepo.Close()
gbr, err := GetBlobBySHA(db.DefaultContext, repo, gitRepo, "65f1bf27bc3bf70f64657658635e66094edbcb4d")
- expectedGBR := &api.GitBlob{
+ expectedGBR := &api.GitBlobResponse{
Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK",
Encoding: "base64",
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d",
@@ -202,43 +200,3 @@ func TestGetBlobBySHA(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, expectedGBR, gbr)
}
-
-func TestGetBlobsBySHA(t *testing.T) {
- unittest.PrepareTestEnv(t)
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
-
- gitRepo, err := gitrepo.OpenRepository(db.DefaultContext, repo)
- require.NoError(t, err)
- defer gitRepo.Close()
-
- gbr, err := GetBlobsBySHA(db.DefaultContext, repo, gitRepo, []string{
- "ea82fc8777a24b07c26b3a4bf4e2742c03733eab", // Home.md
- "6395b68e1feebb1e4c657b4f9f6ba2676a283c0b", // line.svg
- "26f842bcad37fa40a1bb34cbb5ee219ee35d863d", // test.xml
- })
- expectedGBR := []*api.GitBlob{
- {
- Content: "IyBIb21lIHBhZ2UKClRoaXMgaXMgdGhlIGhvbWUgcGFnZSEK",
- Encoding: "base64",
- URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/ea82fc8777a24b07c26b3a4bf4e2742c03733eab",
- SHA: "ea82fc8777a24b07c26b3a4bf4e2742c03733eab",
- Size: 36,
- },
- {
- Content: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZwogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHdpZHRoPSIxMjgiCiAgIGhlaWdodD0iMTI4IgogICB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+CgogIDxsaW5lIHgxPSIwIiB5MT0iNyIgeDI9IjEwIiB5Mj0iNyIgc3Ryb2tlLXdpZHRoPSIxLjUiLz4KPC9zdmc+",
- Encoding: "base64",
- URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b",
- SHA: "6395b68e1feebb1e4c657b4f9f6ba2676a283c0b",
- Size: 246,
- },
- {
- Content: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHRlc3Q+VGhpcyBpcyBYTUw8L3Rlc3Q+Cg==",
- Encoding: "base64",
- URL: "https://try.gitea.io/api/v1/repos/user2/repo2/git/blobs/26f842bcad37fa40a1bb34cbb5ee219ee35d863d",
- SHA: "26f842bcad37fa40a1bb34cbb5ee219ee35d863d",
- Size: 64,
- },
- }
- require.NoError(t, err)
- assert.Equal(t, expectedGBR, gbr)
-}
diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go
index 67c63803d3..d6265273c7 100644
--- a/services/repository/files/diff_test.go
+++ b/services/repository/files/diff_test.go
@@ -124,7 +124,7 @@ func TestGetDiffPreview(t *testing.T) {
require.NoError(t, err)
bs, err := json.Marshal(diff)
require.NoError(t, err)
- assert.Equal(t, string(expectedBs), string(bs))
+ assert.EqualValues(t, string(expectedBs), string(bs))
})
t.Run("empty branch, same results", func(t *testing.T) {
@@ -134,7 +134,7 @@ func TestGetDiffPreview(t *testing.T) {
require.NoError(t, err)
bs, err := json.Marshal(diff)
require.NoError(t, err)
- assert.Equal(t, expectedBs, bs)
+ assert.EqualValues(t, expectedBs, bs)
})
}
diff --git a/services/repository/files/file.go b/services/repository/files/file.go
index 5b93258840..810c60163d 100644
--- a/services/repository/files/file.go
+++ b/services/repository/files/file.go
@@ -5,7 +5,7 @@ package files
import (
"context"
- "errors"
+ "fmt"
"net/url"
"strings"
"time"
@@ -50,10 +50,10 @@ func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index in
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
func GetFileCommitResponse(repo *repo_model.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
if repo == nil {
- return nil, errors.New("repo cannot be nil")
+ return nil, fmt.Errorf("repo cannot be nil")
}
if commit == nil {
- return nil, errors.New("commit cannot be nil")
+ return nil, fmt.Errorf("commit cannot be nil")
}
commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()))
commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.Tree.ID.String()))
@@ -104,35 +104,36 @@ func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *user_m
// then we use bogus User objects for them to store their FullName and Email.
// If only one of the two are provided, we set both of them to it.
// If neither are provided, both are the doer.
- getUser := func(identity *IdentityOptions) *user_model.User {
- if identity == nil || identity.Email == "" {
- return nil
- }
-
- if doer != nil && strings.EqualFold(doer.Email, identity.Email) {
- user := doer // the committer is the doer, so will use their user object
- if identity.Name != "" {
- user.FullName = identity.Name
+ if committer != nil && committer.Email != "" {
+ if doer != nil && strings.EqualFold(doer.Email, committer.Email) {
+ committerUser = doer // the committer is the doer, so will use their user object
+ if committer.Name != "" {
+ committerUser.FullName = committer.Name
}
// Use the provided email and not revert to placeholder mail.
- user.KeepEmailPrivate = false
- return user
- }
-
- var id int64
- if doer != nil {
- id = doer.ID
- }
- return &user_model.User{
- ID: id, // Needed to ensure the doer is checked to pass rules for instance signing of CRUD actions.
- FullName: identity.Name,
- Email: identity.Email,
+ committerUser.KeepEmailPrivate = false
+ } else {
+ committerUser = &user_model.User{
+ FullName: committer.Name,
+ Email: committer.Email,
+ }
+ }
+ }
+ if author != nil && author.Email != "" {
+ if doer != nil && strings.EqualFold(doer.Email, author.Email) {
+ authorUser = doer // the author is the doer, so will use their user object
+ if authorUser.Name != "" {
+ authorUser.FullName = author.Name
+ }
+ // Use the provided email and not revert to placeholder mail.
+ authorUser.KeepEmailPrivate = false
+ } else {
+ authorUser = &user_model.User{
+ FullName: author.Name,
+ Email: author.Email,
+ }
}
}
-
- committerUser = getUser(committer)
- authorUser = getUser(author)
-
if authorUser == nil {
if committerUser != nil {
authorUser = committerUser // No valid author was given so use the committer
diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go
index 169cafba0d..db2f4403f4 100644
--- a/services/repository/files/file_test.go
+++ b/services/repository/files/file_test.go
@@ -14,13 +14,13 @@ func TestCleanUploadFileName(t *testing.T) {
name := "this/is/test"
cleanName := CleanUploadFileName(name)
expectedCleanName := name
- assert.Equal(t, expectedCleanName, cleanName)
+ assert.EqualValues(t, expectedCleanName, cleanName)
})
t.Run("Clean a .git path", func(t *testing.T) {
name := "this/is/test/.git"
cleanName := CleanUploadFileName(name)
expectedCleanName := ""
- assert.Equal(t, expectedCleanName, cleanName)
+ assert.EqualValues(t, expectedCleanName, cleanName)
})
}
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index 18b5226c02..5b1dd65b5a 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -147,7 +147,11 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
stdout := &strings.Builder{}
stderr := &strings.Builder{}
- cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary", "-3")
+ cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary")
+ if git.CheckGitVersionAtLeast("2.32") == nil {
+ cmdApply.AddArguments("-3")
+ }
+
if err := cmdApply.Run(&git.RunOpts{
Dir: t.basePath,
Stdout: stdout,
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 64d3e5887d..b3aadbc6cb 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -6,7 +6,6 @@ package files
import (
"bytes"
"context"
- "errors"
"fmt"
"io"
"os"
@@ -369,7 +368,7 @@ func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) {
// GetBranchCommit Gets the commit object of the given branch
func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, error) {
if t.gitRepo == nil {
- return nil, errors.New("repository has not been cloned")
+ return nil, fmt.Errorf("repository has not been cloned")
}
return t.gitRepo.GetBranchCommit(branch)
}
@@ -377,7 +376,7 @@ func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit,
// GetCommit Gets the commit object of the given commit ID
func (t *TemporaryUploadRepository) GetCommit(commitID string) (*git.Commit, error) {
if t.gitRepo == nil {
- return nil, errors.New("repository has not been cloned")
+ return nil, fmt.Errorf("repository has not been cloned")
}
return t.gitRepo.GetCommit(commitID)
}
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index 5a369b27a5..1e575f95e8 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -87,7 +87,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
if entries[e].IsDir() {
copy(treeURL[copyPos:], entries[e].ID.String())
tree.Entries[i].URL = string(treeURL)
- } else if entries[e].IsSubmodule() {
+ } else if entries[e].IsSubModule() {
// In Github Rest API Version=2022-11-28, if a tree entry is a submodule,
// its url will be returned as an empty string.
// So the URL will be set to "" here.
diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go
index 5cd628722b..7865fcf2e2 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -48,5 +48,5 @@ func TestGetTreeBySHA(t *testing.T) {
TotalCount: 1,
}
- assert.Equal(t, expectedTree, tree)
+ assert.EqualValues(t, expectedTree, tree)
}
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index 8fb9644fa4..5e8834c6de 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -193,34 +193,28 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
if hasOldBranch {
- // Get the current commit of the original branch
- actualBaseCommit, err := t.GetBranchCommit(opts.OldBranch)
+ // Get the commit of the original branch
+ commit, err := t.GetBranchCommit(opts.OldBranch)
if err != nil {
return nil, err // Couldn't get a commit for the branch
}
- var lastKnownCommit git.ObjectID // when nil, the sha provided in the opts.Files must match the current blob-sha
- if opts.OldBranch != opts.NewBranch {
- // when creating a new branch, ignore if a file has been changed in the meantime
- // (such changes will visible when doing the merge)
- lastKnownCommit = actualBaseCommit.ID
- } else if opts.LastCommitID != "" {
- lastKnownCommit, err = t.gitRepo.ConvertToGitID(opts.LastCommitID)
+ // Assigned LastCommitID in opts if it hasn't been set
+ if opts.LastCommitID == "" {
+ opts.LastCommitID = commit.ID.String()
+ } else {
+ lastCommitID, err := t.gitRepo.ConvertToGitID(opts.LastCommitID)
if err != nil {
return nil, fmt.Errorf("ConvertToSHA1: Invalid last commit ID: %w", err)
}
+ opts.LastCommitID = lastCommitID.String()
}
for _, file := range opts.Files {
- if err := handleCheckErrors(file, actualBaseCommit, lastKnownCommit); err != nil {
+ if err := handleCheckErrors(file, commit, opts); err != nil {
return nil, err
}
}
-
- if opts.LastCommitID == "" {
- // needed for t.CommitTree
- opts.LastCommitID = actualBaseCommit.ID.String()
- }
}
contentStore := lfs.NewContentStore()
@@ -283,9 +277,9 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// handles the check for various issues for ChangeRepoFiles
-func handleCheckErrors(file *ChangeRepoFile, actualBaseCommit *git.Commit, lastKnownCommit git.ObjectID) error {
+func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
if file.Operation == "update" || file.Operation == "delete" {
- fromEntry, err := actualBaseCommit.GetTreeEntryByPath(file.Options.fromTreePath)
+ fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
if err != nil {
return err
}
@@ -298,22 +292,22 @@ func handleCheckErrors(file *ChangeRepoFile, actualBaseCommit *git.Commit, lastK
CurrentSHA: fromEntry.ID.String(),
}
}
- } else if lastKnownCommit != nil {
- if actualBaseCommit.ID.String() != lastKnownCommit.String() {
- // If a lastKnownCommit was given and it doesn't match the actualBaseCommit,
- // check if the file has been changed in between
- if changed, err := actualBaseCommit.FileChangedSinceCommit(file.Options.treePath, lastKnownCommit.String()); err != nil {
+ } else if opts.LastCommitID != "" {
+ // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
+ // an error, but only if we aren't creating a new branch.
+ if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
+ if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil {
return err
} else if changed {
return models.ErrCommitIDDoesNotMatch{
- GivenCommitID: lastKnownCommit.String(),
- CurrentCommitID: actualBaseCommit.ID.String(),
+ GivenCommitID: opts.LastCommitID,
+ CurrentCommitID: opts.LastCommitID,
}
}
- // The file wasn't modified, so we are good to update it
+ // The file wasn't modified, so we are good to delete it
}
} else {
- // When updating a file, a lastKnownCommit or SHA needs to be given to make sure other commits
+ // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits
// haven't been made. We throw an error if one wasn't provided.
return models.ErrSHAOrCommitIDNotProvided{}
}
@@ -328,7 +322,7 @@ func handleCheckErrors(file *ChangeRepoFile, actualBaseCommit *git.Commit, lastK
subTreePath := ""
for index, part := range treePathParts {
subTreePath = path.Join(subTreePath, part)
- entry, err := actualBaseCommit.GetTreeEntryByPath(subTreePath)
+ entry, err := commit.GetTreeEntryByPath(subTreePath)
if err != nil {
if git.IsErrNotExist(err) {
// Means there is no item with that name, so we're good
diff --git a/services/repository/fork_test.go b/services/repository/fork_test.go
index 6de241e4d4..227dd1850e 100644
--- a/services/repository/fork_test.go
+++ b/services/repository/fork_test.go
@@ -11,7 +11,6 @@ import (
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/setting"
- "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -37,7 +36,7 @@ func TestForkRepository(t *testing.T) {
assert.False(t, repo_model.IsErrReachLimitOfRepo(err))
// change AllowForkWithoutMaximumLimit to false for the test
- defer test.MockVariableValue(&setting.Repository.AllowForkWithoutMaximumLimit, false)()
+ setting.Repository.AllowForkWithoutMaximumLimit = false
// user has reached maximum limit of repositories
user.MaxRepoCreation = 0
fork2, err := ForkRepositoryAndUpdates(git.DefaultContext, user, user, ForkRepoOptions{
diff --git a/services/repository/generate.go b/services/repository/generate.go
index e23e294de1..9aeb057c3d 100644
--- a/services/repository/generate.go
+++ b/services/repository/generate.go
@@ -43,8 +43,12 @@ type expansion struct {
var defaultTransformers = []transformer{
{Name: "SNAKE", Transform: xstrings.ToSnakeCase},
{Name: "KEBAB", Transform: xstrings.ToKebabCase},
- {Name: "CAMEL", Transform: xstrings.ToCamelCase},
- {Name: "PASCAL", Transform: xstrings.ToPascalCase},
+ // as of xstrings v1.5.0 the CAMEL & PASCAL workarounds are no longer necessary
+ // and can be removed https://codeberg.org/forgejo/forgejo/pulls/4050
+ {Name: "CAMEL", Transform: func(str string) string {
+ return xstrings.FirstRuneToLower(xstrings.ToCamelCase(str))
+ }},
+ {Name: "PASCAL", Transform: xstrings.ToCamelCase},
{Name: "LOWER", Transform: strings.ToLower},
{Name: "UPPER", Transform: strings.ToUpper},
{Name: "TITLE", Transform: util.ToTitleCase},
diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go
index 2eb3a55e96..b0f97d0ffb 100644
--- a/services/repository/generate_test.go
+++ b/services/repository/generate_test.go
@@ -65,30 +65,3 @@ func TestFileNameSanitize(t *testing.T) {
assert.Equal(t, "_", fileNameSanitize("\u0000"))
assert.Equal(t, "ç›®æ ‡", fileNameSanitize("ç›®æ ‡"))
}
-
-func TestTransformers(t *testing.T) {
- input := "Foo_Forgejo-BAR"
-
- tests := []struct {
- name string
- expected string
- }{
- {"SNAKE", "foo_forgejo_bar"},
- {"KEBAB", "foo-forgejo-bar"},
- {"CAMEL", "fooForgejoBar"},
- {"PASCAL", "FooForgejoBar"},
- {"LOWER", "foo_forgejo-bar"},
- {"UPPER", "FOO_FORGEJO-BAR"},
- {"TITLE", "Foo_forgejo-Bar"},
- }
-
- for i, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- tranform := defaultTransformers[i]
- assert.Equal(t, tt.name, tranform.Name)
-
- got := tranform.Transform(input)
- assert.Equal(t, tt.expected, got)
- })
- }
-}
diff --git a/services/repository/lfs.go b/services/repository/lfs.go
index 2e090290a7..43acb8ee6c 100644
--- a/services/repository/lfs.go
+++ b/services/repository/lfs.go
@@ -76,7 +76,7 @@ func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.R
err = git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, metaObject *git_model.LFSMetaObject) error {
total++
- pointerSha := git.ComputeBlobHash(objectFormat, []byte(metaObject.StringContent()))
+ pointerSha := git.ComputeBlobHash(objectFormat, []byte(metaObject.Pointer.StringContent()))
if gitRepo.IsObjectExist(pointerSha.String()) {
return git_model.MarkLFSMetaObject(ctx, metaObject.ID)
diff --git a/services/repository/push.go b/services/repository/push.go
index eaedd80e1f..53574a7d93 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -66,7 +66,7 @@ func PushUpdates(opts []*repo_module.PushUpdateOptions) error {
for _, opt := range opts {
if opt.IsNewRef() && opt.IsDelRef() {
- return errors.New("Old and new revisions are both NULL")
+ return fmt.Errorf("Old and new revisions are both NULL")
}
}
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 0d5ce647e0..a2620740b1 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -6,7 +6,6 @@ package repository
import (
"context"
- "errors"
"fmt"
"forgejo.org/models/db"
@@ -73,10 +72,10 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN
if ok, err := organization.CanCreateOrgRepo(ctx, owner.ID, authUser.ID); err != nil {
return nil, err
} else if !ok {
- return nil, errors.New("cannot push-create repository for org")
+ return nil, fmt.Errorf("cannot push-create repository for org")
}
} else if authUser.ID != owner.ID {
- return nil, errors.New("cannot push-create repository for another user")
+ return nil, fmt.Errorf("cannot push-create repository for another user")
}
}
@@ -119,17 +118,6 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili
return committer.Commit()
}
-// ConvertMirrorToNormalRepo converts a mirror to a normal repo
-func ConvertMirrorToNormalRepo(ctx context.Context, repo *repo_model.Repository) (err error) {
- repo.IsMirror = false
-
- if _, err := CleanUpMigrateInfo(ctx, repo); err != nil {
- return err
- }
-
- return repo_model.DeleteMirrorByRepoID(ctx, repo.ID)
-}
-
// LinkedRepository returns the linked repo if any
func LinkedRepository(ctx context.Context, a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) {
if a.IssueID != 0 {
diff --git a/services/repository/repository_test.go b/services/repository/repository_test.go
index 9f71bdec23..c08f7151ca 100644
--- a/services/repository/repository_test.go
+++ b/services/repository/repository_test.go
@@ -41,16 +41,3 @@ func TestLinkedRepository(t *testing.T) {
})
}
}
-
-func TestConvertMirrorToNormalRepo(t *testing.T) {
- require.NoError(t, unittest.PrepareTestDatabase())
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
- repo.IsMirror = true
- err := repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "is_mirror")
-
- require.NoError(t, err)
-
- err = ConvertMirrorToNormalRepo(db.DefaultContext, repo)
- require.NoError(t, err)
- assert.False(t, repo.IsMirror)
-}
diff --git a/services/repository/setting.go b/services/repository/setting.go
index 68cdfc370b..c127f3129e 100644
--- a/services/repository/setting.go
+++ b/services/repository/setting.go
@@ -7,6 +7,7 @@ import (
"context"
"slices"
+ actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unit"
@@ -28,7 +29,7 @@ func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, uni
}
if slices.Contains(deleteUnitTypes, unit.TypeActions) {
- if err := actions_service.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
+ if err := actions_model.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
}
diff --git a/services/repository/sync_fork.go b/services/repository/sync_fork.go
deleted file mode 100644
index ebcac76136..0000000000
--- a/services/repository/sync_fork.go
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright 2025 The Forgejo Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repository
-
-import (
- "context"
- "fmt"
- "slices"
-
- git_model "forgejo.org/models/git"
- repo_model "forgejo.org/models/repo"
- user_model "forgejo.org/models/user"
- "forgejo.org/modules/git"
- repo_module "forgejo.org/modules/repository"
- api "forgejo.org/modules/structs"
-)
-
-// SyncFork syncs a branch of a fork with the base repo
-func SyncFork(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) error {
- err := repo.MustNotBeArchived()
- if err != nil {
- return err
- }
-
- err = repo.GetBaseRepo(ctx)
- if err != nil {
- return err
- }
-
- err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{
- Remote: repo.RepoPath(),
- Branch: fmt.Sprintf("%s:%s", branch, branch),
- Env: repo_module.PushingEnvironment(doer, repo),
- })
-
- return err
-}
-
-// CanSyncFork returns information about syncing a fork
-func GetSyncForkInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*api.SyncForkInfo, error) {
- info := new(api.SyncForkInfo)
-
- if !repo.IsFork {
- return info, nil
- }
-
- if repo.IsArchived {
- return info, nil
- }
-
- err := repo.GetBaseRepo(ctx)
- if err != nil {
- return nil, err
- }
-
- forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch)
- if err != nil {
- return nil, err
- }
-
- info.ForkCommit = forkBranch.CommitID
-
- baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch)
- if err != nil {
- if git_model.IsErrBranchNotExist(err) {
- // If the base repo don't have the branch, we don't need to continue
- return info, nil
- }
- return nil, err
- }
-
- info.BaseCommit = baseBranch.CommitID
-
- // If both branches has the same latest commit, we don't need to sync
- if forkBranch.CommitID == baseBranch.CommitID {
- return info, nil
- }
-
- // Check if the latest commit of the fork is also in the base
- gitRepo, err := git.OpenRepository(ctx, repo.BaseRepo.RepoPath())
- if err != nil {
- return nil, err
- }
- defer gitRepo.Close()
-
- commit, err := gitRepo.GetCommit(forkBranch.CommitID)
- if err != nil {
- if git.IsErrNotExist(err) {
- return info, nil
- }
- return nil, err
- }
-
- branchList, err := commit.GetAllBranches()
- if err != nil {
- return nil, err
- }
-
- if !slices.Contains(branchList, branch) {
- return info, nil
- }
-
- diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID, nil)
- if err != nil {
- return nil, err
- }
-
- info.Allowed = true
- info.CommitsBehind = diff.Behind
-
- return info, nil
-}
diff --git a/services/secrets/secrets.go b/services/secrets/secrets.go
index 2de83ef5a2..2d5aebdbc1 100644
--- a/services/secrets/secrets.go
+++ b/services/secrets/secrets.go
@@ -15,16 +15,16 @@ func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data
return nil, false, err
}
- s, exists, err := db.Get[secret_model.Secret](ctx, secret_model.FindSecretsOptions{
+ s, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{
OwnerID: ownerID,
RepoID: repoID,
Name: name,
- }.ToConds())
+ })
if err != nil {
return nil, false, err
}
- if !exists {
+ if len(s) == 0 {
s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, name, data)
if err != nil {
return nil, false, err
@@ -32,11 +32,11 @@ func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data
return s, true, nil
}
- s.SetSecret(data)
- if _, err := db.GetEngine(ctx).Cols("data").ID(s.ID).Update(s); err != nil {
+ if err := secret_model.UpdateSecret(ctx, s[0].ID, data); err != nil {
return nil, false, err
}
- return s, false, nil
+
+ return s[0], false, nil
}
func DeleteSecretByID(ctx context.Context, ownerID, repoID, secretID int64) error {
diff --git a/services/shared/automerge/automerge.go b/services/shared/automerge/automerge.go
index be7b2f6eb4..1dc309f4b3 100644
--- a/services/shared/automerge/automerge.go
+++ b/services/shared/automerge/automerge.go
@@ -21,9 +21,9 @@ import (
var PRAutoMergeQueue *queue.WorkerPoolQueue[string]
func addToQueue(pr *issues_model.PullRequest, sha string) {
- log.Trace("Adding pullID: %d to the automerge queue with sha %s", pr.ID, sha)
+ log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
if err := PRAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
- log.Error("Error adding pullID: %d to the automerge queue %v", pr.ID, err)
+ log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
}
}
@@ -43,29 +43,32 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m
return nil
}
+// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
return
}
- commitID := pull.HeadCommitID
- if commitID == "" {
- commitID = getCommitIDFromRefName(ctx, pull)
+ if err := pull.LoadBaseRepo(ctx); err != nil {
+ log.Error("LoadBaseRepo: %v", err)
+ return
}
- if commitID == "" {
+ gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
+ if err != nil {
+ log.Error("OpenRepository: %v", err)
+ return
+ }
+ defer gitRepo.Close()
+ commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
+ if err != nil {
+ log.Error("GetRefCommitID: %v", err)
return
}
addToQueue(pull, commitID)
}
-var AddToQueueIfMergeable = func(ctx context.Context, pull *issues_model.PullRequest) {
- if pull.Status == issues_model.PullRequestStatusMergeable {
- StartPRCheckAndAutoMerge(ctx, pull)
- }
-}
-
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
@@ -115,24 +118,3 @@ func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.
return pulls, nil
}
-
-func getCommitIDFromRefName(ctx context.Context, pull *issues_model.PullRequest) string {
- if err := pull.LoadBaseRepo(ctx); err != nil {
- log.Error("LoadBaseRepo: %v", err)
- return ""
- }
-
- gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
- if err != nil {
- log.Error("OpenRepository: %v", err)
- return ""
- }
- defer gitRepo.Close()
- commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
- if err != nil {
- log.Error("GetRefCommitID: %v", err)
- return ""
- }
-
- return commitID
-}
diff --git a/services/task/task.go b/services/task/task.go
index f030bdb38c..3181fc79d7 100644
--- a/services/task/task.go
+++ b/services/task/task.go
@@ -5,7 +5,6 @@ package task
import (
"context"
- "errors"
"fmt"
admin_model "forgejo.org/models/admin"
@@ -42,7 +41,7 @@ func Run(ctx context.Context, t *admin_model.Task) error {
func Init() error {
taskQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "task", handler)
if taskQueue == nil {
- return errors.New("unable to create task queue")
+ return fmt.Errorf("unable to create task queue")
}
go graceful.GetManager().RunWithCancel(taskQueue)
return nil
diff --git a/services/user/TestReplaceInactivePrimaryEmail/forgejo_auth_token.yml b/services/user/TestReplaceInactivePrimaryEmail/forgejo_auth_token.yml
deleted file mode 100644
index 93a6c184b6..0000000000
--- a/services/user/TestReplaceInactivePrimaryEmail/forgejo_auth_token.yml
+++ /dev/null
@@ -1,5 +0,0 @@
--
- id: 1001
- uid: 10
- lookup_key: unique
- purpose: user_activation
diff --git a/services/user/avatar_test.go b/services/user/avatar_test.go
index 17132a74ab..b208efeb6f 100644
--- a/services/user/avatar_test.go
+++ b/services/user/avatar_test.go
@@ -42,7 +42,7 @@ func TestUserDeleteAvatar(t *testing.T) {
err := UploadAvatar(db.DefaultContext, user, buff.Bytes())
require.NoError(t, err)
verification := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- assert.NotEmpty(t, verification.Avatar)
+ assert.NotEqual(t, "", verification.Avatar)
// fail to delete ...
storage.Avatars = storage.UninitializedStorage
@@ -60,7 +60,7 @@ func TestUserDeleteAvatar(t *testing.T) {
// ... the avatar is removed from the database
verification = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- assert.Empty(t, verification.Avatar)
+ assert.Equal(t, "", verification.Avatar)
})
t.Run("Success", func(t *testing.T) {
@@ -70,12 +70,12 @@ func TestUserDeleteAvatar(t *testing.T) {
err := UploadAvatar(db.DefaultContext, user, buff.Bytes())
require.NoError(t, err)
verification := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- assert.NotEmpty(t, verification.Avatar)
+ assert.NotEqual(t, "", verification.Avatar)
err = DeleteAvatar(db.DefaultContext, user)
require.NoError(t, err)
verification = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- assert.Empty(t, verification.Avatar)
+ assert.Equal(t, "", verification.Avatar)
})
}
diff --git a/services/user/delete.go b/services/user/delete.go
index bed7abde07..9ce917cd27 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -1,5 +1,4 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
-// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@@ -217,11 +216,6 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
}
// ***** END: ExternalLoginUser *****
- // If the user was reported as abusive, a shadow copy should be created before deletion.
- if err = user_model.IfNeededCreateShadowCopyForUser(ctx, u.ID, u); err != nil {
- return err
- }
-
if _, err = db.DeleteByID[user_model.User](ctx, u.ID); err != nil {
return fmt.Errorf("delete: %w", err)
}
diff --git a/services/user/email.go b/services/user/email.go
index cc38dacf95..f49efde1be 100644
--- a/services/user/email.go
+++ b/services/user/email.go
@@ -1,5 +1,4 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
-// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@@ -9,7 +8,6 @@ import (
"errors"
"strings"
- auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
user_model "forgejo.org/models/user"
"forgejo.org/modules/setting"
@@ -172,11 +170,6 @@ func ReplaceInactivePrimaryEmail(ctx context.Context, oldEmail string, email *us
return err
}
- // Delete previous activation token.
- if err := auth_model.DeleteAuthTokenByUser(ctx, user.ID); err != nil {
- return err
- }
-
return DeleteEmailAddresses(ctx, user, []string{oldEmail})
}
@@ -210,11 +203,6 @@ func MakeEmailAddressPrimary(ctx context.Context, u *user_model.User, newPrimary
oldPrimaryEmail := u.Email
- // If the user was reported as abusive, a shadow copy should be created before first update (of certain columns).
- if err = user_model.IfNeededCreateShadowCopyForUser(ctx, u.ID, u, "email"); err != nil {
- return err
- }
-
// 1. Update user table
u.Email = newPrimaryEmail.Email
if _, err = sess.ID(u.ID).Cols("email").Update(u); err != nil {
diff --git a/services/user/email_test.go b/services/user/email_test.go
index d2cff22696..b48936a27e 100644
--- a/services/user/email_test.go
+++ b/services/user/email_test.go
@@ -6,7 +6,6 @@ package user
import (
"testing"
- auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
organization_model "forgejo.org/models/organization"
"forgejo.org/models/unittest"
@@ -124,34 +123,25 @@ func TestAddEmailAddresses(t *testing.T) {
}
func TestReplaceInactivePrimaryEmail(t *testing.T) {
- defer unittest.OverrideFixtures("services/user/TestReplaceInactivePrimaryEmail/")()
require.NoError(t, unittest.PrepareTestDatabase())
- t.Run("User doesn't exist", func(t *testing.T) {
- email := &user_model.EmailAddress{
- Email: "user9999999@example.com",
- UID: 9999999,
- }
- err := ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
- require.Error(t, err)
- assert.True(t, user_model.IsErrUserNotExist(err))
- })
+ email := &user_model.EmailAddress{
+ Email: "user9999999@example.com",
+ UID: 9999999,
+ }
+ err := ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
+ require.Error(t, err)
+ assert.True(t, user_model.IsErrUserNotExist(err))
- t.Run("Normal", func(t *testing.T) {
- unittest.AssertExistsIf(t, true, &auth_model.AuthorizationToken{UID: 10})
+ email = &user_model.EmailAddress{
+ Email: "user201@example.com",
+ UID: 10,
+ }
+ err = ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
+ require.NoError(t, err)
- email := &user_model.EmailAddress{
- Email: "user201@example.com",
- UID: 10,
- }
- err := ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
- require.NoError(t, err)
-
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
- assert.Equal(t, "user201@example.com", user.Email)
-
- unittest.AssertExistsIf(t, false, &auth_model.AuthorizationToken{UID: 10})
- })
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
+ assert.Equal(t, "user201@example.com", user.Email)
}
func TestDeleteEmailAddresses(t *testing.T) {
diff --git a/services/user/user.go b/services/user/user.go
index 9cb6858f0c..d90fbac978 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -13,7 +13,6 @@ import (
"forgejo.org/models"
asymkey_model "forgejo.org/models/asymkey"
- "forgejo.org/models/auth"
"forgejo.org/models/db"
"forgejo.org/models/organization"
packages_model "forgejo.org/models/packages"
@@ -26,7 +25,6 @@ import (
"forgejo.org/modules/storage"
"forgejo.org/modules/util"
"forgejo.org/services/agit"
- "forgejo.org/services/auth/source/oauth2"
org_service "forgejo.org/services/org"
"forgejo.org/services/packages"
container_service "forgejo.org/services/packages/container"
@@ -49,28 +47,10 @@ func renameUser(ctx context.Context, u *user_model.User, newUserName string, doe
}
// Non-local users are not allowed to change their username.
- // If the doer is an admin, then allow the rename - they know better.
- if !doerIsAdmin && !u.IsOrganization() && !u.IsLocal() {
- // If the user's authentication source is OAuth2 and that source allows for
- // username changes then don't make a fuzz about it.
-
- if !u.IsOAuth2() {
- return user_model.ErrUserIsNotLocal{
- UID: u.ID,
- Name: u.Name,
- }
- }
-
- source, err := auth.GetSourceByID(ctx, u.LoginSource)
- if err != nil {
- return err
- }
- sourceCfg := source.Cfg.(*oauth2.Source)
- if !sourceCfg.AllowUsernameChange {
- return user_model.ErrUserIsNotLocal{
- UID: u.ID,
- Name: u.Name,
- }
+ if !u.IsOrganization() && !u.IsLocal() {
+ return user_model.ErrUserIsNotLocal{
+ UID: u.ID,
+ Name: u.Name,
}
}
diff --git a/services/user/user_test.go b/services/user/user_test.go
index 7240813411..ebcdb17710 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -15,18 +15,13 @@ import (
asymkey_model "forgejo.org/models/asymkey"
"forgejo.org/models/auth"
"forgejo.org/models/db"
- "forgejo.org/models/moderation"
"forgejo.org/models/organization"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
- "forgejo.org/modules/json"
- "forgejo.org/modules/optional"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"forgejo.org/modules/timeutil"
- "forgejo.org/services/auth/source/oauth2"
- redirect_service "forgejo.org/services/redirect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -72,7 +67,13 @@ func TestDeleteUser(t *testing.T) {
}
func TestPurgeUser(t *testing.T) {
- defer unittest.OverrideFixtures("services/user/TestPurgeUser")()
+ defer unittest.OverrideFixtures(
+ unittest.FixturesOptions{
+ Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
+ Base: setting.AppWorkPath,
+ Dirs: []string{"services/user/TestPurgeUser/"},
+ },
+ )()
require.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
defer test.MockVariableValue(&setting.SSH.CreateAuthorizedKeysFile, true)()
@@ -142,10 +143,17 @@ func TestCreateUser(t *testing.T) {
}
func TestRenameUser(t *testing.T) {
- defer unittest.OverrideFixtures("models/user/fixtures/")()
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 21})
+ t.Run("Non-Local", func(t *testing.T) {
+ u := &user_model.User{
+ Type: user_model.UserTypeIndividual,
+ LoginType: auth.OAuth2,
+ }
+ require.ErrorIs(t, RenameUser(db.DefaultContext, u, "user_rename"), user_model.ErrUserIsNotLocal{})
+ })
+
t.Run("Same username", func(t *testing.T) {
require.NoError(t, RenameUser(db.DefaultContext, user, user.Name))
})
@@ -185,9 +193,9 @@ func TestRenameUser(t *testing.T) {
require.NoError(t, RenameUser(db.DefaultContext, user, newUsername))
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: newUsername, LowerName: strings.ToLower(newUsername)})
- redirectUID, err := redirect_service.LookupUserRedirect(db.DefaultContext, user, oldUsername)
+ redirectUID, err := user_model.LookupUserRedirect(db.DefaultContext, oldUsername)
require.NoError(t, err)
- assert.Equal(t, user.ID, redirectUID)
+ assert.EqualValues(t, user.ID, redirectUID)
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})
})
@@ -203,41 +211,17 @@ func TestRenameUser(t *testing.T) {
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "user_rename"})
// The granularity of created_unix is a second.
- test.SleepTillNextSecond()
+ time.Sleep(time.Second)
require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-2"))
unittest.AssertExistsIf(t, false, &user_model.Redirect{LowerName: "user_rename"})
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"})
setting.Service.MaxUserRedirects = 2
- test.SleepTillNextSecond()
+ time.Sleep(time.Second)
require.NoError(t, RenameUser(db.DefaultContext, user, "redirect-3"))
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-1"})
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "redirect-2"})
})
-
- t.Run("Non-local", func(t *testing.T) {
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1041, LoginSource: 1001})
- authSource := unittest.AssertExistsAndLoadBean(t, &auth.Source{ID: user.LoginSource})
- assert.False(t, user.IsLocal())
- assert.True(t, user.IsOAuth2())
-
- t.Run("Allowed", func(t *testing.T) {
- require.NoError(t, RenameUser(t.Context(), user, "I-am-a-local-username"))
- })
-
- t.Run("Not allowed", func(t *testing.T) {
- authSourceCfg := authSource.Cfg.(*oauth2.Source)
- authSourceCfg.AllowUsernameChange = false
- authSource.Cfg = authSourceCfg
- _, err := db.GetEngine(t.Context()).Cols("cfg").ID(authSource.ID).Update(authSource)
- require.NoError(t, err)
-
- require.ErrorIs(t, RenameUser(t.Context(), user, "Another-username-change"), user_model.ErrUserIsNotLocal{UID: user.ID, Name: user.Name})
- t.Run("Admin", func(t *testing.T) {
- require.NoError(t, AdminRenameUser(t.Context(), user, "Another-username-change"))
- })
- })
- })
}
func TestCreateUser_Issue5882(t *testing.T) {
@@ -299,56 +283,3 @@ func TestDeleteInactiveUsers(t *testing.T) {
unittest.AssertExistsIf(t, true, newUser)
unittest.AssertExistsIf(t, true, newEmail)
}
-
-func TestCreateShadowCopyOnUserUpdate(t *testing.T) {
- defer unittest.OverrideFixtures("models/fixtures/ModerationFeatures")()
- require.NoError(t, unittest.PrepareTestDatabase())
-
- userAlexSmithID := int64(1002)
- abuseReportID := int64(2) // submitted for @alexsmith
- newDummyValue := "[REDACTED]" // used for updating profile text fields
-
- // Retrieve the abusive user (@alexsmith) and the abuse report already created for this user.
- abuser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userAlexSmithID})
- report := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{
- ID: abuseReportID,
- ContentType: moderation.ReportedContentTypeUser,
- ContentID: abuser.ID,
- })
- // The report should not already have a shadow copy linked.
- assert.False(t, report.ShadowCopyID.Valid)
-
- // Keep a copy of old field values before updating them.
- oldUserData := user_model.UserData{
- FullName: abuser.FullName,
- Location: abuser.Location,
- Website: abuser.Website,
- Pronouns: abuser.Pronouns,
- Description: abuser.Description,
- }
-
- // The abusive user is updating their profile.
- opts := &UpdateOptions{
- FullName: optional.Some(newDummyValue),
- Location: optional.Some(newDummyValue),
- Website: optional.Some(newDummyValue),
- Pronouns: optional.Some(newDummyValue),
- Description: optional.Some(newDummyValue),
- }
- require.NoError(t, UpdateUser(t.Context(), abuser, opts))
-
- // Reload the report.
- report = unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReport{ID: report.ID})
- // A shadow copy should have been created and linked to our report.
- assert.True(t, report.ShadowCopyID.Valid)
- // Retrieve the newly created shadow copy and unmarshal the stored JSON so that we can check the values.
- shadowCopy := unittest.AssertExistsAndLoadBean(t, &moderation.AbuseReportShadowCopy{ID: report.ShadowCopyID.Int64})
- shadowCopyUserData := new(user_model.UserData)
- require.NoError(t, json.Unmarshal([]byte(shadowCopy.RawValue), &shadowCopyUserData))
- // Check to see if the initial field values of the user were stored within the shadow copy.
- assert.Equal(t, oldUserData.FullName, shadowCopyUserData.FullName)
- assert.Equal(t, oldUserData.Location, shadowCopyUserData.Location)
- assert.Equal(t, oldUserData.Website, shadowCopyUserData.Website)
- assert.Equal(t, oldUserData.Pronouns, shadowCopyUserData.Pronouns)
- assert.Equal(t, oldUserData.Description, shadowCopyUserData.Description)
-}
diff --git a/services/webhook/default.go b/services/webhook/default.go
index 797b98f99a..30717a7352 100644
--- a/services/webhook/default.go
+++ b/services/webhook/default.go
@@ -36,7 +36,8 @@ func (dh defaultHandler) Type() webhook_module.HookType {
func (dh defaultHandler) Icon(size int) template.HTML {
if dh.forgejo {
- return svg.RenderHTML("gitea-forgejo", size, "img")
+ // forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work
+ return shared.ImgIcon("forgejo.svg", size)
}
return svg.RenderHTML("gitea-gitea", size, "img")
}
diff --git a/services/webhook/default_test.go b/services/webhook/default_test.go
index fcef4612e1..f946870d57 100644
--- a/services/webhook/default_test.go
+++ b/services/webhook/default_test.go
@@ -237,7 +237,7 @@ func TestOpenProjectPayload(t *testing.T) {
assert.Equal(t, 12, j.Get("number").MustBeValid().ToInt())
assert.Equal(t, "http://localhost:3000/test/repo/pulls/12", j.Get("html_url").MustBeValid().ToString())
assert.Equal(t, jsoniter.NilValue, j.Get("updated_at").ValueType())
- assert.Empty(t, j.Get("state").MustBeValid().ToString())
+ assert.Equal(t, "", j.Get("state").MustBeValid().ToString())
assert.Equal(t, "Fix bug", j.Get("title").MustBeValid().ToString())
assert.Equal(t, "fixes bug #2", j.Get("body").MustBeValid().ToString())
diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go
index 23aca80345..0c7c039f10 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -6,7 +6,6 @@ package webhook
import (
"context"
"crypto/tls"
- "errors"
"fmt"
"io"
"net/http"
@@ -219,7 +218,7 @@ func Init() error {
hookQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "webhook_sender", handler)
if hookQueue == nil {
- return errors.New("unable to create webhook_sender queue")
+ return fmt.Errorf("unable to create webhook_sender queue")
}
go graceful.GetManager().RunWithCancel(hookQueue)
diff --git a/services/webhook/deliver_test.go b/services/webhook/deliver_test.go
index 1a9ce05de4..4dd898e60a 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -137,7 +137,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
// Version 1
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
- assert.Empty(t, r.Header.Get("Content-Type"))
+ assert.Equal(t, "", r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, `{"data": 42}`, string(body))
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index ec53c79c2c..9d5c7e573f 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -207,12 +207,6 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
}
-func (dc dingtalkConvertor) Action(p *api.ActionPayload) (DingtalkPayload, error) {
- text, _ := getActionPayloadInfo(p, noneLinkFormatter)
-
- return createDingtalkPayload(text, text, "view action", p.Run.HTMLURL), nil
-}
-
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
return DingtalkPayload{
MsgType: "actionCard",
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 7259c4a995..3970a2552d 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -16,7 +16,6 @@ import (
"unicode/utf8"
webhook_model "forgejo.org/models/webhook"
- "forgejo.org/modules/base"
"forgejo.org/modules/git"
"forgejo.org/modules/json"
"forgejo.org/modules/log"
@@ -152,18 +151,6 @@ var (
redColor = color("ff3232")
)
-// https://discord.com/developers/docs/resources/message#embed-object-embed-limits
-// Discord has some limits in place for the embeds.
-// According to some tests, there is no consistent limit for different character sets.
-// For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
-// To keep it simple, we currently truncate at 2000.
-const discordDescriptionCharactersLimit = 2000
-
-type discordConvertor struct {
- Username string
- AvatarURL string
-}
-
// Create implements PayloadConvertor Create method
func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) {
// created tag/branch
@@ -325,10 +312,9 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error)
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
}
-func (d discordConvertor) Action(p *api.ActionPayload) (DiscordPayload, error) {
- text, color := getActionPayloadInfo(p, noneLinkFormatter)
-
- return d.createPayload(p.Run.TriggerUser, text, "", p.Run.HTMLURL, color), nil
+type discordConvertor struct {
+ Username string
+ AvatarURL string
}
var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{}
@@ -350,7 +336,7 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
case webhook_module.HookEventPullRequestReviewApproved:
return "approved", nil
case webhook_module.HookEventPullRequestReviewRejected:
- return "requested changes", nil
+ return "rejected", nil
case webhook_module.HookEventPullRequestReviewComment:
return "comment", nil
default:
@@ -371,7 +357,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
Embeds: []DiscordEmbed{
{
Title: title,
- Description: base.TruncateString(text, discordDescriptionCharactersLimit),
+ Description: text,
URL: url,
Color: color,
Author: DiscordEmbedAuthor{
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index b04be30bc6..ce3aaa10cf 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -175,7 +175,7 @@ func TestDiscordPayload(t *testing.T) {
require.NoError(t, err)
assert.Len(t, pl.Embeds, 1)
- assert.Len(t, pl.Embeds[0].Description, 2000)
+ assert.Len(t, pl.Embeds[0].Description, 4096)
})
t.Run("IssueComment", func(t *testing.T) {
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 57f2362783..01b3d07983 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -191,12 +191,6 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
return newFeishuTextPayload(text), nil
}
-func (fc feishuConvertor) Action(p *api.ActionPayload) (FeishuPayload, error) {
- text, _ := getActionPayloadInfo(p, noneLinkFormatter)
-
- return newFeishuTextPayload(text), nil
-}
-
type feishuConvertor struct{}
var _ shared.PayloadConvertor[FeishuPayload] = feishuConvertor{}
diff --git a/services/webhook/general.go b/services/webhook/general.go
index c728b6ba1a..40a2467177 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -37,12 +37,11 @@ func getPullRequestInfo(p *api.PullRequestPayload) (title, link, by, operator, o
for i, user := range assignList {
assignStringList[i] = user.UserName
}
- switch p.Action {
- case api.HookIssueAssigned:
+ if p.Action == api.HookIssueAssigned {
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
- case api.HookIssueUnassigned:
+ } else if p.Action == api.HookIssueUnassigned {
operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
- case api.HookIssueMilestoned:
+ } else if p.Action == api.HookIssueMilestoned {
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID)
}
link = p.PullRequest.HTMLURL
@@ -63,12 +62,11 @@ func getIssuesInfo(p *api.IssuePayload) (issueTitle, link, by, operator, operate
for i, user := range assignList {
assignStringList[i] = user.UserName
}
- switch p.Action {
- case api.HookIssueAssigned:
+ if p.Action == api.HookIssueAssigned {
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
- case api.HookIssueUnassigned:
+ } else if p.Action == api.HookIssueUnassigned {
operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
- case api.HookIssueMilestoned:
+ } else if p.Action == api.HookIssueMilestoned {
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID)
}
link = p.Issue.HTMLURL
@@ -304,25 +302,6 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
return text, color
}
-func getActionPayloadInfo(p *api.ActionPayload, linkFormatter linkFormatter) (text string, color int) {
- runLink := linkFormatter(p.Run.HTMLURL, p.Run.Title)
- repoLink := linkFormatter(p.Run.Repo.HTMLURL, p.Run.Repo.FullName)
-
- switch p.Action {
- case api.HookActionFailure:
- text = fmt.Sprintf("%s Action Failed in %s %s", runLink, repoLink, p.Run.PrettyRef)
- color = redColor
- case api.HookActionRecover:
- text = fmt.Sprintf("%s Action Recovered in %s %s", runLink, repoLink, p.Run.PrettyRef)
- color = greenColor
- case api.HookActionSuccess:
- text = fmt.Sprintf("%s Action Succeeded in %s %s", runLink, repoLink, p.Run.PrettyRef)
- color = greenColor
- }
-
- return text, color
-}
-
// ToHook convert models.Webhook to api.Hook
// This function is not part of the convert package to prevent an import cycle
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
diff --git a/services/webhook/general_test.go b/services/webhook/general_test.go
index 10c779742d..b321fb3f8c 100644
--- a/services/webhook/general_test.go
+++ b/services/webhook/general_test.go
@@ -270,22 +270,6 @@ func pullReleaseTestPayload() *api.ReleasePayload {
}
}
-func ActionTestPayload() *api.ActionPayload {
- // this is not a complete action payload but enough for testing purposes
- return &api.ActionPayload{
- Run: &api.ActionRun{
- Repo: &api.Repository{
- HTMLURL: "http://localhost:3000/test/repo",
- Name: "repo",
- FullName: "test/repo",
- },
- PrettyRef: "main",
- HTMLURL: "http://localhost:3000/test/repo/actions/runs/69",
- Title: "Build release",
- },
- }
-}
-
func pullRequestTestPayload() *api.PullRequestPayload {
return &api.PullRequestPayload{
Action: api.HookIssueOpened,
@@ -691,36 +675,3 @@ func TestGetIssueCommentPayloadInfo(t *testing.T) {
assert.Equal(t, c.color, color, "case %d", i)
}
}
-
-func TestGetActionPayloadInfo(t *testing.T) {
- p := ActionTestPayload()
-
- cases := []struct {
- action api.HookActionAction
- text string
- color int
- }{
- {
- api.HookActionFailure,
- "Build release Action Failed in test/repo main",
- redColor,
- },
- {
- api.HookActionSuccess,
- "Build release Action Succeeded in test/repo main",
- greenColor,
- },
- {
- api.HookActionRecover,
- "Build release Action Recovered in test/repo main",
- greenColor,
- },
- }
-
- for i, c := range cases {
- p.Action = c.action
- text, color := getActionPayloadInfo(p, noneLinkFormatter)
- assert.Equal(t, c.text, text, "case %d", i)
- assert.Equal(t, c.color, color, "case %d", i)
- }
-}
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index bdb0c292ab..f1cc9384d3 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -273,12 +273,6 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
return m.newPayload(text)
}
-func (m matrixConvertor) Action(p *api.ActionPayload) (MatrixPayload, error) {
- text, _ := getActionPayloadInfo(p, htmlLinkFormatter)
-
- return m.newPayload(text)
-}
-
var urlRegex = regexp.MustCompile(`]*?href="([^">]*?)">(.*?) `)
func getMessageBody(htmlText string) string {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 3b35c407e1..1ed03afd26 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -326,23 +326,6 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error)
), nil
}
-func (m msteamsConvertor) Action(p *api.ActionPayload) (MSTeamsPayload, error) {
- title, color := getActionPayloadInfo(p, noneLinkFormatter)
-
- // TODO: is TriggerUser correct here?
- // if you'd like to test these proprietary services, see the discussion on: https://codeberg.org/forgejo/forgejo/pulls/7508
- return createMSTeamsPayload(
- p.Run.Repo,
- p.Run.TriggerUser,
- title,
- "",
- p.Run.HTMLURL,
- color,
- // TODO: does this make any sense?
- &MSTeamsFact{"Action:", p.Run.Title},
- ), nil
-}
-
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
facts := make([]MSTeamsFact, 0, 2)
if r != nil {
diff --git a/services/webhook/msteams_test.go b/services/webhook/msteams_test.go
index da6439f198..b210f299bc 100644
--- a/services/webhook/msteams_test.go
+++ b/services/webhook/msteams_test.go
@@ -335,7 +335,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Equal(t, "[test/repo] New wiki page 'index' (Wiki change comment)", pl.Summary)
assert.Len(t, pl.Sections, 1)
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.Sections[0].Text)
+ assert.Equal(t, "", pl.Sections[0].Text)
assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
@@ -356,7 +356,7 @@ func TestMSTeamsPayload(t *testing.T) {
assert.Equal(t, "[test/repo] Wiki page 'index' edited (Wiki change comment)", pl.Summary)
assert.Len(t, pl.Sections, 1)
assert.Equal(t, "user1", pl.Sections[0].ActivitySubtitle)
- assert.Empty(t, pl.Sections[0].Text)
+ assert.Equal(t, "", pl.Sections[0].Text)
assert.Len(t, pl.Sections[0].Facts, 2)
for _, fact := range pl.Sections[0].Facts {
if fact.Name == "Repository:" {
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 009efc994f..e9fd52c940 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -6,7 +6,6 @@ package webhook
import (
"context"
- actions_model "forgejo.org/models/actions"
issues_model "forgejo.org/models/issues"
packages_model "forgejo.org/models/packages"
"forgejo.org/models/perm"
@@ -888,45 +887,6 @@ func (m *webhookNotifier) PackageDelete(ctx context.Context, doer *user_model.Us
notifyPackage(ctx, doer, pd, api.HookPackageDeleted)
}
-func (m *webhookNotifier) ActionRunNowDone(ctx context.Context, run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) {
- source := EventSource{
- Repository: run.Repo,
- Owner: run.TriggerUser,
- }
-
- // The doer is the one whose perspective is used to view this ActionRun.
- // In the best case we use the user that created the webhook.
- // Unfortunately we don't know who that was.
- // So instead we use the repo owner, who is able to create webhooks and allow others to do so by making them repo admins.
- // This is pretty close to perfect.
- doer := run.Repo.Owner
-
- payload := &api.ActionPayload{
- Run: convert.ToActionRun(ctx, run, doer),
- LastRun: convert.ToActionRun(ctx, lastRun, doer),
- PriorStatus: priorStatus.String(),
- }
-
- if run.Status.IsSuccess() {
- payload.Action = api.HookActionSuccess
- if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunSuccess, payload); err != nil {
- log.Error("PrepareWebhooks: %v", err)
- }
- // send another event when this is a recover
- if lastRun != nil && !lastRun.Status.IsSuccess() {
- payload.Action = api.HookActionRecover
- if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunRecover, payload); err != nil {
- log.Error("PrepareWebhooks: %v", err)
- }
- }
- } else {
- payload.Action = api.HookActionFailure
- if err := PrepareWebhooks(ctx, source, webhook_module.HookEventActionRunFailure, payload); err != nil {
- log.Error("PrepareWebhooks: %v", err)
- }
- }
-}
-
func notifyPackage(ctx context.Context, sender *user_model.User, pd *packages_model.PackageDescriptor, action api.HookPackageAction) {
source := EventSource{
Repository: pd.Repository,
diff --git a/services/webhook/notifier_test.go b/services/webhook/notifier_test.go
index a810de91c1..6b1f170287 100644
--- a/services/webhook/notifier_test.go
+++ b/services/webhook/notifier_test.go
@@ -4,9 +4,9 @@
package webhook
import (
+ "path/filepath"
"testing"
- actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
@@ -14,12 +14,10 @@ import (
webhook_model "forgejo.org/models/webhook"
"forgejo.org/modules/git"
"forgejo.org/modules/json"
- "forgejo.org/modules/log"
"forgejo.org/modules/repository"
"forgejo.org/modules/setting"
"forgejo.org/modules/structs"
"forgejo.org/modules/test"
- webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -58,7 +56,13 @@ func pushCommits() *repository.PushCommits {
}
func TestSyncPushCommits(t *testing.T) {
- defer unittest.OverrideFixtures("services/webhook/TestPushCommits")()
+ defer unittest.OverrideFixtures(
+ unittest.FixturesOptions{
+ Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
+ Base: setting.AppWorkPath,
+ Dirs: []string{"services/webhook/TestPushCommits"},
+ },
+ )()
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -86,12 +90,18 @@ func TestSyncPushCommits(t *testing.T) {
var payloadContent structs.PushPayload
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
assert.Len(t, payloadContent.Commits, 1)
- assert.Equal(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
+ assert.EqualValues(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
})
}
func TestPushCommits(t *testing.T) {
- defer unittest.OverrideFixtures("services/webhook/TestPushCommits")()
+ defer unittest.OverrideFixtures(
+ unittest.FixturesOptions{
+ Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
+ Base: setting.AppWorkPath,
+ Dirs: []string{"services/webhook/TestPushCommits"},
+ },
+ )()
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -119,193 +129,6 @@ func TestPushCommits(t *testing.T) {
var payloadContent structs.PushPayload
require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
assert.Len(t, payloadContent.Commits, 1)
- assert.Equal(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
- })
-}
-
-func assertActionEqual(t *testing.T, expectedRun *actions_model.ActionRun, actualRun *structs.ActionRun) {
- assert.NotNil(t, expectedRun)
- assert.NotNil(t, actualRun)
- // only test a few things
- assert.Equal(t, expectedRun.ID, actualRun.ID)
- assert.Equal(t, expectedRun.Status.String(), actualRun.Status)
- assert.Equal(t, expectedRun.Index, actualRun.Index)
- assert.Equal(t, expectedRun.RepoID, actualRun.Repo.ID)
- // convert to unix because of time zones
- assert.Equal(t, expectedRun.Stopped.AsTime().Unix(), actualRun.Stopped.Unix())
- assert.Equal(t, expectedRun.Title, actualRun.Title)
- assert.Equal(t, expectedRun.WorkflowID, actualRun.WorkflowID)
-}
-
-func TestAction(t *testing.T) {
- defer unittest.OverrideFixtures("services/webhook/TestPushCommits")()
- require.NoError(t, unittest.PrepareTestDatabase())
-
- triggerUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
- repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerID: triggerUser.ID})
-
- oldSuccessRun := &actions_model.ActionRun{
- ID: 1,
- Status: actions_model.StatusSuccess,
- Index: 1,
- RepoID: repo.ID,
- Stopped: 1693648027,
- WorkflowID: "some_workflow",
- Title: "oldSuccessRun",
- TriggerUser: triggerUser,
- TriggerUserID: triggerUser.ID,
- TriggerEvent: "push",
- }
- oldSuccessRun.LoadAttributes(db.DefaultContext)
- oldFailureRun := &actions_model.ActionRun{
- ID: 1,
- Status: actions_model.StatusFailure,
- Index: 1,
- RepoID: repo.ID,
- Stopped: 1693648027,
- WorkflowID: "some_workflow",
- Title: "oldFailureRun",
- TriggerUser: triggerUser,
- TriggerUserID: triggerUser.ID,
- TriggerEvent: "push",
- }
- oldFailureRun.LoadAttributes(db.DefaultContext)
- newSuccessRun := &actions_model.ActionRun{
- ID: 1,
- Status: actions_model.StatusSuccess,
- Index: 1,
- RepoID: repo.ID,
- Stopped: 1693648327,
- WorkflowID: "some_workflow",
- Title: "newSuccessRun",
- TriggerUser: triggerUser,
- TriggerUserID: triggerUser.ID,
- TriggerEvent: "push",
- }
- newSuccessRun.LoadAttributes(db.DefaultContext)
- newFailureRun := &actions_model.ActionRun{
- ID: 1,
- Status: actions_model.StatusFailure,
- Index: 1,
- RepoID: repo.ID,
- Stopped: 1693648327,
- WorkflowID: "some_workflow",
- Title: "newFailureRun",
- TriggerUser: triggerUser,
- TriggerUserID: triggerUser.ID,
- TriggerEvent: "push",
- }
- newFailureRun.LoadAttributes(db.DefaultContext)
-
- t.Run("Successful Run after Nothing", func(t *testing.T) {
- defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
-
- NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, nil)
-
- // there's only one of these at the time
- hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%'"))
- assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType)
-
- var payloadContent structs.ActionPayload
- require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
- assert.Equal(t, structs.HookActionSuccess, payloadContent.Action)
- assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
- assertActionEqual(t, newSuccessRun, payloadContent.Run)
- assert.Nil(t, payloadContent.LastRun)
- })
-
- t.Run("Successful Run after Failure", func(t *testing.T) {
- defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
-
- NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, oldFailureRun)
-
- {
- hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%oldFailureRun%'"))
- assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType)
-
- var payloadContent structs.ActionPayload
- require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
- assert.Equal(t, structs.HookActionSuccess, payloadContent.Action)
- assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
- assertActionEqual(t, newSuccessRun, payloadContent.Run)
- assertActionEqual(t, oldFailureRun, payloadContent.LastRun)
- }
- {
- hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_recover' AND payload_content LIKE '%recover%newSuccessRun%oldFailureRun%'"))
- assert.Equal(t, webhook_module.HookEventActionRunRecover, hookTask.EventType)
-
- log.Error("something: %s", hookTask.PayloadContent)
- var payloadContent structs.ActionPayload
- require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
- assert.Equal(t, structs.HookActionRecover, payloadContent.Action)
- assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
- assertActionEqual(t, newSuccessRun, payloadContent.Run)
- assertActionEqual(t, oldFailureRun, payloadContent.LastRun)
- }
- })
-
- t.Run("Successful Run after Success", func(t *testing.T) {
- defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
-
- NewNotifier().ActionRunNowDone(db.DefaultContext, newSuccessRun, actions_model.StatusWaiting, oldSuccessRun)
-
- hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_success' AND payload_content LIKE '%success%newSuccessRun%oldSuccessRun%'"))
- assert.Equal(t, webhook_module.HookEventActionRunSuccess, hookTask.EventType)
-
- var payloadContent structs.ActionPayload
- require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
- assert.Equal(t, structs.HookActionSuccess, payloadContent.Action)
- assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
- assertActionEqual(t, newSuccessRun, payloadContent.Run)
- assertActionEqual(t, oldSuccessRun, payloadContent.LastRun)
- })
-
- t.Run("Failed Run after Nothing", func(t *testing.T) {
- defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
-
- NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, nil)
-
- // there should only be this one at the time
- hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%'"))
- assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType)
-
- var payloadContent structs.ActionPayload
- require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
- assert.Equal(t, structs.HookActionFailure, payloadContent.Action)
- assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
- assertActionEqual(t, newFailureRun, payloadContent.Run)
- assert.Nil(t, payloadContent.LastRun)
- })
-
- t.Run("Failed Run after Failure", func(t *testing.T) {
- defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
-
- NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, oldFailureRun)
-
- hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%oldFailureRun%'"))
- assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType)
-
- var payloadContent structs.ActionPayload
- require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
- assert.Equal(t, structs.HookActionFailure, payloadContent.Action)
- assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
- assertActionEqual(t, newFailureRun, payloadContent.Run)
- assertActionEqual(t, oldFailureRun, payloadContent.LastRun)
- })
-
- t.Run("Failed Run after Success", func(t *testing.T) {
- defer test.MockVariableValue(&setting.Webhook.PayloadCommitLimit, 10)()
-
- NewNotifier().ActionRunNowDone(db.DefaultContext, newFailureRun, actions_model.StatusWaiting, oldSuccessRun)
-
- hookTask := unittest.AssertExistsAndLoadBean(t, &webhook_model.HookTask{}, unittest.Cond("event_type == 'action_run_failure' AND payload_content LIKE '%failure%newFailureRun%oldSuccessRun%'"))
- assert.Equal(t, webhook_module.HookEventActionRunFailure, hookTask.EventType)
-
- var payloadContent structs.ActionPayload
- require.NoError(t, json.Unmarshal([]byte(hookTask.PayloadContent), &payloadContent))
- assert.Equal(t, structs.HookActionFailure, payloadContent.Action)
- assert.Equal(t, actions_model.StatusWaiting.String(), payloadContent.PriorStatus)
- assertActionEqual(t, newFailureRun, payloadContent.Run)
- assertActionEqual(t, oldSuccessRun, payloadContent.LastRun)
+ assert.EqualValues(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
})
}
diff --git a/services/webhook/shared/payloader.go b/services/webhook/shared/payloader.go
index e3be4c4b4c..0a6535eddb 100644
--- a/services/webhook/shared/payloader.go
+++ b/services/webhook/shared/payloader.go
@@ -36,7 +36,6 @@ type PayloadConvertor[T any] interface {
Release(*api.ReleasePayload) (T, error)
Wiki(*api.WikiPayload) (T, error)
Package(*api.PackagePayload) (T, error)
- Action(*api.ActionPayload) (T, error)
}
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
@@ -87,8 +86,6 @@ func NewPayload[T any](rc PayloadConvertor[T], data []byte, event webhook_module
return convertUnmarshalledJSON(rc.Wiki, data)
case webhook_module.HookEventPackage:
return convertUnmarshalledJSON(rc.Package, data)
- case webhook_module.HookEventActionRunFailure, webhook_module.HookEventActionRunRecover, webhook_module.HookEventActionRunSuccess:
- return convertUnmarshalledJSON(rc.Action, data)
}
var t T
return t, fmt.Errorf("newPayload unsupported event: %s", event)
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 8c61e7ba25..e854f89c6c 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -142,7 +142,6 @@ func SlackLinkToRef(repoURL, ref string) string {
return SlackLinkFormatter(url, refName)
}
-// TODO: fix spelling to Converter
// Create implements payloadConvertor Create method
func (s slackConvertor) Create(p *api.CreatePayload) (SlackPayload, error) {
refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
@@ -312,12 +311,6 @@ func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, erro
return s.createPayload(text, nil), nil
}
-func (s slackConvertor) Action(p *api.ActionPayload) (SlackPayload, error) {
- text, _ := getActionPayloadInfo(p, SlackLinkFormatter)
-
- return s.createPayload(text, nil), nil
-}
-
func (s slackConvertor) createPayload(text string, attachments []SlackAttachment) SlackPayload {
return SlackPayload{
Channel: s.Channel,
diff --git a/services/webhook/sourcehut/builds.go b/services/webhook/sourcehut/builds.go
index 96ed55c8ff..bd3eeebc6c 100644
--- a/services/webhook/sourcehut/builds.go
+++ b/services/webhook/sourcehut/builds.go
@@ -26,7 +26,7 @@ import (
"forgejo.org/services/webhook/shared"
"code.forgejo.org/go-chi/binding"
- "go.yaml.in/yaml/v3"
+ "gopkg.in/yaml.v3"
)
type BuildsHandler struct{}
@@ -190,10 +190,6 @@ func (pc sourcehutConvertor) Package(_ *api.PackagePayload) (graphqlPayload[buil
return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported
}
-func (pc sourcehutConvertor) Action(_ *api.ActionPayload) (graphqlPayload[buildsVariables], error) {
- return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported
-}
-
// newPayload opens and adjusts the manifest to submit to the builds service
//
// in case of an error the Error field will be set, to be visible by the end-user under recent deliveries
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index e6897f68bc..d0abd667f4 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -205,12 +205,6 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro
return createTelegramPayload(text), nil
}
-func (telegramConvertor) Action(p *api.ActionPayload) (TelegramPayload, error) {
- text, _ := getActionPayloadInfo(p, htmlLinkFormatter)
-
- return createTelegramPayload(text), nil
-}
-
func createTelegramPayload(message string) TelegramPayload {
return TelegramPayload{
Message: markup.Sanitize(strings.TrimSpace(message)),
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index ecbbfcfbd6..989b535564 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -103,7 +103,7 @@ type EventSource struct {
Owner *user_model.User
}
-// handler delivers hook tasks
+// handle delivers hook tasks
func handler(items ...int64) []int64 {
ctx := graceful.GetManager().HammerContext()
diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go
index 15cb8f620c..c9af09d3e9 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -14,7 +14,6 @@ import (
webhook_model "forgejo.org/models/webhook"
"forgejo.org/modules/setting"
api "forgejo.org/modules/structs"
- "forgejo.org/modules/test"
webhook_module "forgejo.org/modules/webhook"
"forgejo.org/services/convert"
@@ -105,8 +104,7 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
func TestWebhookUserMail(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
- defer test.MockVariableValue(&setting.Service.NoReplyAddress, "no-reply.com")()
-
+ setting.Service.NoReplyAddress = "no-reply.com"
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email)
assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email)
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 5c765b0754..323d23aba7 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -201,12 +201,6 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
return newWechatworkMarkdownPayload(text), nil
}
-func (wc wechatworkConvertor) Action(p *api.ActionPayload) (WechatworkPayload, error) {
- text, _ := getActionPayloadInfo(p, noneLinkFormatter)
-
- return newWechatworkMarkdownPayload(text), nil
-}
-
type wechatworkConvertor struct{}
var _ shared.PayloadConvertor[WechatworkPayload] = wechatworkConvertor{}
diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go
index cb984425af..d76104dfc7 100644
--- a/services/wiki/wiki_test.go
+++ b/services/wiki/wiki_test.go
@@ -26,7 +26,7 @@ func TestMain(m *testing.M) {
func TestWebPathSegments(t *testing.T) {
a := WebPathSegments("a%2Fa/b+c/d-e/f-g.-")
- assert.Equal(t, []string{"a/a", "b c", "d e", "f-g"}, a)
+ assert.EqualValues(t, []string{"a/a", "b c", "d e", "f-g"}, a)
}
func TestUserTitleToWebPath(t *testing.T) {
@@ -63,7 +63,7 @@ func TestWebPathToDisplayName(t *testing.T) {
{"a b", "a%20b.md"},
} {
_, displayName := WebPathToUserTitle(test.WebPath)
- assert.Equal(t, test.Expected, displayName)
+ assert.EqualValues(t, test.Expected, displayName)
}
}
@@ -80,7 +80,7 @@ func TestWebPathToGitPath(t *testing.T) {
{"2000-01-02-meeting.md", "2000-01-02+meeting"},
{"2000-01-02 meeting.-.md", "2000-01-02%20meeting.-"},
} {
- assert.Equal(t, test.Expected, WebPathToGitPath(test.WikiName))
+ assert.EqualValues(t, test.Expected, WebPathToGitPath(test.WikiName))
}
}
@@ -134,9 +134,9 @@ func TestUserWebGitPathConsistency(t *testing.T) {
_, userTitle1 := WebPathToUserTitle(webPath1)
gitPath1 := WebPathToGitPath(webPath1)
- assert.Equal(t, userTitle, userTitle1, "UserTitle for userTitle: %q", userTitle)
- assert.Equal(t, webPath, webPath1, "WebPath for userTitle: %q", userTitle)
- assert.Equal(t, gitPath, gitPath1, "GitPath for userTitle: %q", userTitle)
+ assert.EqualValues(t, userTitle, userTitle1, "UserTitle for userTitle: %q", userTitle)
+ assert.EqualValues(t, webPath, webPath1, "WebPath for userTitle: %q", userTitle)
+ assert.EqualValues(t, gitPath, gitPath1, "GitPath for userTitle: %q", userTitle)
}
}
@@ -175,7 +175,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
gitPath := WebPathToGitPath(webPath)
entry, err := masterTree.GetTreeEntryByPath(gitPath)
require.NoError(t, err)
- assert.Equal(t, gitPath, entry.Name(), "%s not added correctly", userTitle)
+ assert.EqualValues(t, gitPath, entry.Name(), "%s not added correctly", userTitle)
})
}
@@ -220,7 +220,7 @@ func TestRepository_EditWikiPage(t *testing.T) {
gitPath := WebPathToGitPath(webPath)
entry, err := masterTree.GetTreeEntryByPath(gitPath)
require.NoError(t, err)
- assert.Equal(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName)
+ assert.EqualValues(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName)
if newWikiName != "Home" {
_, err := masterTree.GetTreeEntryByPath("Home.md")
@@ -284,12 +284,12 @@ func TestPrepareWikiFileName(t *testing.T) {
}
if existence != tt.existence {
if existence {
- t.Error("expect to find no escaped file but we detect one")
+ t.Errorf("expect to find no escaped file but we detect one")
} else {
- t.Error("expect to find an escaped file but we could not detect one")
+ t.Errorf("expect to find an escaped file but we could not detect one")
}
}
- assert.Equal(t, tt.wikiPath, newWikiPath)
+ assert.EqualValues(t, tt.wikiPath, newWikiPath)
})
}
}
@@ -311,13 +311,13 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home")
assert.False(t, existence)
require.NoError(t, err)
- assert.Equal(t, "Home.md", newWikiPath)
+ assert.EqualValues(t, "Home.md", newWikiPath)
}
func TestWebPathConversion(t *testing.T) {
assert.Equal(t, "path/wiki", WebPathToURLPath(WebPath("path/wiki")))
assert.Equal(t, "wiki", WebPathToURLPath(WebPath("wiki")))
- assert.Empty(t, WebPathToURLPath(WebPath("")))
+ assert.Equal(t, "", WebPathToURLPath(WebPath("")))
}
func TestWebPathFromRequest(t *testing.T) {
diff --git a/shell.nix b/shell.nix
deleted file mode 100644
index a96ef516a2..0000000000
--- a/shell.nix
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- pkgs ? import { },
-}:
-
-pkgs.mkShell {
- name = "forgejo";
- nativeBuildInputs = with pkgs; [
- # generic
- git
- git-lfs
- gnumake
- gnused
- gnutar
- gzip
-
- # frontend
- nodejs
-
- # backend
- gofumpt
- sqlite
- go
- gopls
- gotestsum
-
- # tests
- openssh
- ];
-}
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index d4eaa29117..1ca5573cae 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -299,13 +299,6 @@
{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}
-
-
-
{{ctx.Locale.Tr "admin.auths.allow_username_change"}}
-
-
{{ctx.Locale.Tr "admin.auths.allow_username_change.description"}}
-
-
{{ctx.Locale.Tr "admin.auths.oauth2_use_custom_url"}}
@@ -404,7 +397,7 @@
{{ctx.Locale.Tr "admin.auths.update"}}
- {{ctx.Locale.Tr "admin.auths.delete"}}
+ {{ctx.Locale.Tr "admin.auths.delete"}}
@@ -421,7 +414,7 @@
-
+
-
-
-
{{ctx.Locale.Tr "admin.auths.allow_username_change"}}
-
-
{{ctx.Locale.Tr "admin.auths.allow_username_change.description"}}
-
-
diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl
index 36d44f21f3..8f2b1c12e3 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -51,16 +51,6 @@
-
-
-
- {{ctx.Locale.Tr "admin.config.global_2fa_requirement.title"}}
- {{ctx.Locale.Tr (print "admin.config.global_2fa_requirement." .GlobalTwoFactorRequirement)}}
-
-
-
@@ -257,16 +247,6 @@
-
-
-
- {{ctx.Locale.Tr "enabled"}}
- {{if .Moderation.Enabled}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}
-
-
-
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 0a9a28fa2d..5c30df87af 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -62,7 +62,7 @@
@@ -104,7 +104,7 @@
-
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 6b6b463a6b..5f9965e34c 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -72,7 +72,7 @@
{{ctx.Locale.TrSize .CalculateBlobSize}}
{{DateUtils.AbsoluteShort .Version.CreatedUnix}}
- {{svg "octicon-trash"}}
+ {{svg "octicon-trash"}}
{{else}}
{{ctx.Locale.Tr "repo.pulls.no_results"}}
@@ -84,7 +84,7 @@
{{template "base/paginate" .}}
-
+
-
+
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index 57c0c210cc..afe8e6942a 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -35,7 +35,7 @@
{{end}}
-