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"`
}
// Rerun will rerun jobs in the given run
@@ -383,7 +493,7 @@ func Rerun(ctx *context_module.Context) {
run.PreviousDuration = run.Duration()
run.Started = 0
run.Stopped = 0
- if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
+ if err := actions_service.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
@@ -395,6 +505,7 @@ 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
@@ -402,13 +513,31 @@ 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
@@ -416,9 +545,22 @@ 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
+ }
+ }
}
- ctx.JSON(http.StatusOK, struct{}{})
+ if redirectURL != "" {
+ ctx.JSON(http.StatusOK, &redirectObject{Redirect: redirectURL})
+ } else {
+ ctx.Error(http.StatusInternalServerError, "unable to determine redirectURL for job rerun")
+ }
}
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
@@ -436,7 +578,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_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
+ _, err := actions_service.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
return err
}); err != nil {
return err
@@ -512,16 +654,16 @@ func Cancel(ctx *context_module.Context) {
if job.TaskID == 0 {
job.Status = actions_model.StatusCancelled
job.Stopped = timeutil.TimeStampNow()
- n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
+ n, err := actions_service.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
if err != nil {
return err
}
if n == 0 {
- return fmt.Errorf("job has changed, try again")
+ return errors.New("job has changed, try again")
}
continue
}
- if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
+ if err := actions_service.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
return err
}
}
@@ -549,13 +691,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_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
+ if err := actions_service.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_model.UpdateRunJob(ctx, job, nil, "status")
+ _, err := actions_service.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
@@ -619,19 +761,27 @@ 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
+ return nil
}
ctx.Error(http.StatusInternalServerError, err.Error())
- return
+ return nil
}
artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
- return
+ return nil
}
artifactsResponse := ArtifactsViewResponse{
Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)),
@@ -647,7 +797,7 @@ func ArtifactsView(ctx *context_module.Context) {
Status: status,
})
}
- ctx.JSON(http.StatusOK, artifactsResponse)
+ return &artifactsResponse
}
func ArtifactsDeleteView(ctx *context_module.Context) {
@@ -668,30 +818,82 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, struct{}{})
}
-func ArtifactsDownloadView(ctx *context_module.Context) {
- 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
+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
}
- artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
- RunID: run.ID,
- ArtifactName: artifactName,
- })
+ run, has, err := actions_model.GetRunByIDWithHas(ctx, runID)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
- return
+ 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 {
- ctx.Error(http.StatusNotFound, "artifact not found")
+ 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() {
+ return
+ }
+ artifactNameOrID := ctx.Params("artifact_name_or_id")
+
+ artifacts := artifactsFindByNameOrID(ctx, run.ID, artifactNameOrID)
+ if ctx.Written() {
return
}
@@ -720,12 +922,14 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
- common.ServeContentByReadSeeker(ctx.Base, artifactName, util.ToPointer(art.UpdatedUnix.AsTime()), f)
+ common.ServeContentByReadSeeker(ctx.Base, artifacts[0].ArtifactName+".zip", 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
new file mode 100644
index 0000000000..e3f72e9e37
--- /dev/null
+++ b/routers/web/repo/actions/view_test.go
@@ -0,0 +1,512 @@
+// 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/activity.go b/routers/web/repo/activity.go
index af9cea0f33..c9cd2c13bb 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -7,10 +7,10 @@ import (
"net/http"
"time"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/services/context"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/base"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index b5078e1f63..5b2eaef889 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -7,18 +7,18 @@ import (
"fmt"
"net/http"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/httpcache"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/routers/common"
- "code.gitea.io/gitea/services/attachment"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- repo_service "code.gitea.io/gitea/services/repository"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/httpcache"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/util"
+ "forgejo.org/routers/common"
+ "forgejo.org/services/attachment"
+ "forgejo.org/services/context"
+ "forgejo.org/services/context/upload"
+ repo_service "forgejo.org/services/repository"
)
// UploadIssueAttachment response for Issue/PR attachments
@@ -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/badges/badges.go b/routers/web/repo/badges/badges.go
index a2306d5836..e623a21fc0 100644
--- a/routers/web/repo/badges/badges.go
+++ b/routers/web/repo/badges/badges.go
@@ -8,11 +8,11 @@ import (
"net/url"
"strings"
- actions_model "code.gitea.io/gitea/models/actions"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/setting"
- context_module "code.gitea.io/gitea/services/context"
+ actions_model "forgejo.org/models/actions"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/setting"
+ context_module "forgejo.org/services/context"
)
func getBadgeURL(ctx *context_module.Context, label, text, color string) string {
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index c7fbaaefcb..f4cc2a2cea 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -10,16 +10,16 @@ import (
"net/url"
"strings"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/highlight"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/context"
- files_service "code.gitea.io/gitea/services/repository/files"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/highlight"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/templates"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/context"
+ files_service "forgejo.org/services/repository/files"
)
type blameRow struct {
@@ -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 4897a5f4fc..0fe52bfb48 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -11,23 +11,23 @@ import (
"net/url"
"strings"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- release_service "code.gitea.io/gitea/services/release"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/routers/utils"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ release_service "forgejo.org/services/release"
+ repo_service "forgejo.org/services/repository"
)
const (
@@ -70,11 +70,6 @@ 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/card.go b/routers/web/repo/card.go
index e73971cd94..449e5c4890 100644
--- a/routers/web/repo/card.go
+++ b/routers/web/repo/card.go
@@ -15,17 +15,17 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- issue_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/card"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/db"
+ issue_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/card"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/services/context"
)
// drawUser draws a user avatar in a summary card
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
index 90dae704f4..0f57eb66f0 100644
--- a/routers/web/repo/cherry_pick.go
+++ b/routers/web/repo/cherry_pick.go
@@ -8,17 +8,17 @@ import (
"errors"
"strings"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/repository/files"
)
var tplCherryPick base.TplName = "repo/editor/cherry_pick"
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
index c76f492da0..44c07e617e 100644
--- a/routers/web/repo/code_frequency.go
+++ b/routers/web/repo/code_frequency.go
@@ -7,9 +7,9 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/services/context"
- contributors_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/modules/base"
+ "forgejo.org/services/context"
+ contributors_service "forgejo.org/services/repository"
)
const (
@@ -34,7 +34,7 @@ func CodeFrequencyData(ctx *context.Context) {
ctx.Status(http.StatusAccepted)
return
}
- ctx.ServerError("GetCodeFrequencyData", err)
+ ctx.ServerError("GetContributorStats", err)
} else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 857e34381e..408a2844de 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -12,26 +12,25 @@ import (
"path"
"strings"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/gitdiff"
- git_service "code.gitea.io/gitea/services/repository"
- "code.gitea.io/gitea/services/repository/gitgraph"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/gitdiff"
+ git_service "forgejo.org/services/repository"
+ "forgejo.org/services/repository/gitgraph"
)
const (
@@ -84,8 +83,19 @@ func Commits(ctx *context.Context) {
ctx.ServerError("CommitsByRange", err)
return
}
- ctx.Data["Commits"] = processGitCommits(ctx, commits)
+ ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository)
+ 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
@@ -202,7 +212,7 @@ func SearchCommits(ctx *context.Context) {
return
}
ctx.Data["CommitCount"] = len(commits)
- ctx.Data["Commits"] = processGitCommits(ctx, commits)
+ ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository)
ctx.Data["Keyword"] = query
if all {
@@ -267,7 +277,7 @@ func FileHistory(ctx *context.Context) {
}
}
- ctx.Data["Commits"] = processGitCommits(ctx, commits)
+ ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(ctx, commits, ctx.Repo.Repository)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
@@ -329,7 +339,7 @@ func Diff(ctx *context.Context) {
maxLines, maxFiles = -1, -1
}
- diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{
+ diff, err := gitdiff.GetDiffFull(ctx, gitRepo, &gitdiff.DiffOptions{
AfterCommitID: commitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
@@ -375,9 +385,6 @@ 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
@@ -456,20 +463,6 @@ 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 24785d867e..feedeef945 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -16,29 +16,29 @@ import (
"path/filepath"
"strings"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/charset"
- csv_module "code.gitea.io/gitea/modules/csv"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/routers/common"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- "code.gitea.io/gitea/services/gitdiff"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/charset"
+ csv_module "forgejo.org/modules/csv"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/typesniffer"
+ "forgejo.org/modules/util"
+ "forgejo.org/routers/common"
+ "forgejo.org/services/context"
+ "forgejo.org/services/context/upload"
+ "forgejo.org/services/gitdiff"
)
const (
@@ -312,22 +312,16 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
if !baseIsCommit && !baseIsBranch && !baseIsTag {
- // Check if baseBranch is short sha commit hash
- if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
- ci.BaseBranch = baseCommit.ID.String()
- ctx.Data["BaseBranch"] = ci.BaseBranch
- baseIsCommit = true
- } else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
+ if 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
@@ -514,15 +508,8 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
headIsBranch := ci.HeadGitRepo.IsBranchExist(ci.HeadBranch)
headIsTag := ci.HeadGitRepo.IsTagExist(ci.HeadBranch)
if !headIsCommit && !headIsBranch && !headIsTag {
- // Check if headBranch is short sha commit hash
- if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
- ci.HeadBranch = headCommit.ID.String()
- ctx.Data["HeadBranch"] = ci.HeadBranch
- headIsCommit = true
- } else {
- ctx.NotFound("IsRefExist", nil)
- return nil
- }
+ ctx.NotFound("IsRefExist", nil)
+ return nil
}
ctx.Data["HeadIsCommit"] = headIsCommit
ctx.Data["HeadIsBranch"] = headIsBranch
@@ -533,17 +520,6 @@ 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
@@ -597,7 +573,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
@@ -621,7 +597,7 @@ func PrepareCompareDiff(
fileOnly := ctx.FormBool("file-only")
- diff, err := gitdiff.GetDiff(ctx, ci.HeadGitRepo,
+ diff, err := gitdiff.GetDiffFull(ctx, ci.HeadGitRepo,
&gitdiff.DiffOptions{
BeforeCommitID: beforeCommitID,
AfterCommitID: headCommitID,
@@ -654,15 +630,15 @@ func PrepareCompareDiff(
return false
}
- commits := processGitCommits(ctx, ci.CompareInfo.Commits)
+ commits := git_model.ParseCommitsWithStatus(ctx, ci.CompareInfo.Commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
if len(commits) == 1 {
c := commits[0]
- title = strings.TrimSpace(c.UserCommit.Summary())
+ title = strings.TrimSpace(c.Summary())
- body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
+ body := strings.Split(strings.TrimSpace(c.Message()), "\n")
if len(body) > 1 {
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
@@ -933,28 +909,33 @@ func ExcerptBlob(ctx *context.Context) {
ctx.Error(http.StatusInternalServerError, "getExcerptLines")
return
}
- if idxRight > lastRight {
+
+ // 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 {
lineText := " "
if rightHunkSize > 0 || leftHunkSize > 0 {
lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
}
lineText = html.EscapeString(lineText)
- lineSection := &gitdiff.DiffLine{
- Type: gitdiff.DiffLineSection,
- Content: lineText,
- SectionInfo: &gitdiff.DiffLineSectionInfo{
- Path: filePath,
- LastLeftIdx: lastLeft,
- LastRightIdx: lastRight,
- LeftIdx: idxLeft,
- RightIdx: idxRight,
- LeftHunkSize: leftHunkSize,
- RightHunkSize: rightHunkSize,
- },
- }
- if direction == "up" {
+ lineSection.Content = lineText
+
+ switch direction {
+ case "up":
section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
- } else if direction == "down" {
+ case "down":
section.Lines = append(section.Lines, lineSection)
}
}
@@ -966,7 +947,7 @@ func ExcerptBlob(ctx *context.Context) {
}
func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chunkSize int) ([]*gitdiff.DiffLine, error) {
- blob, err := commit.Tree.GetBlobByPath(filePath)
+ blob, err := commit.GetBlobByPath(filePath)
if err != nil {
return nil, err
}
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
index 762fbf9379..094d13b54b 100644
--- a/routers/web/repo/contributors.go
+++ b/routers/web/repo/contributors.go
@@ -7,9 +7,9 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/services/context"
- contributors_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/modules/base"
+ "forgejo.org/services/context"
+ contributors_service "forgejo.org/services/repository"
)
const (
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index d7fe368474..9fb4d78fe3 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -7,15 +7,15 @@ package repo
import (
"time"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/httpcache"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/routers/common"
- "code.gitea.io/gitea/services/context"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/httpcache"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/routers/common"
+ "forgejo.org/services/context"
)
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
@@ -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 f27ad62982..3e3cb0016d 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -10,26 +10,26 @@ import (
"path"
"strings"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- "code.gitea.io/gitea/services/forms"
- files_service "code.gitea.io/gitea/services/repository/files"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/typesniffer"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/routers/utils"
+ "forgejo.org/services/context"
+ "forgejo.org/services/context/upload"
+ "forgejo.org/services/forms"
+ files_service "forgejo.org/services/repository/files"
)
const (
@@ -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).IsRepresentableAsText() {
+ if !typesniffer.DetectContentType(buf, blob.Name()).IsRepresentableAsText() {
ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
return
}
@@ -585,7 +585,7 @@ func DeleteFilePost(ctx *context.Context) {
ctx.Error(http.StatusInternalServerError, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
+ ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
} else if git.IsErrPushRejected(err) {
errPushRej := err.(*git.ErrPushRejected)
if len(errPushRej.Message) == 0 {
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index 4d565b5fd6..b5d40abdab 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -6,11 +6,11 @@ package repo
import (
"testing"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/services/contexttest"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -37,7 +37,7 @@ func TestCleanUploadName(t *testing.T) {
"..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
}
for k, v := range kases {
- assert.EqualValues(t, cleanUploadFileName(k), v)
+ assert.Equal(t, cleanUploadFileName(k), v)
}
}
diff --git a/routers/web/repo/find.go b/routers/web/repo/find.go
index 9da4237c1e..808323631c 100644
--- a/routers/web/repo/find.go
+++ b/routers/web/repo/find.go
@@ -6,9 +6,9 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/flags/manage.go b/routers/web/repo/flags/manage.go
index 377a5c20f8..c97ef54818 100644
--- a/routers/web/repo/flags/manage.go
+++ b/routers/web/repo/flags/manage.go
@@ -6,10 +6,10 @@ package flags
import (
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index bced8e61b1..403245596d 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -18,20 +18,21 @@ import (
"sync"
"time"
- actions_model "code.gitea.io/gitea/models/actions"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/context"
- repo_service "code.gitea.io/gitea/services/repository"
+ actions_model "forgejo.org/models/actions"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "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"
)
@@ -111,7 +112,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
return nil
}
- if redirectRepoID, err := repo_model.LookupRedirect(ctx, owner.ID, reponame); err == nil {
+ if redirectRepoID, err := redirect_service.LookupRepoRedirect(ctx, ctx.Doer, owner.ID, reponame); err == nil {
context.RedirectToRepo(ctx.Base, redirectRepoID)
return nil
}
@@ -183,9 +184,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
if repoExist {
// Because of special ref "refs/for" .. , need delay write permission check
- if git.SupportProcReceive {
- accessMode = perm.AccessModeRead
- }
+ accessMode = perm.AccessModeRead
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
@@ -194,24 +193,19 @@ func httpBase(ctx *context.Context) *serviceHandler {
ctx.ServerError("GetTaskByID", err)
return nil
}
- if task.RepoID != repo.ID {
+
+ p, err := access_model.GetActionRepoPermission(ctx, repo, task)
+ if err != nil {
+ ctx.ServerError("GetActionRepoPermission", err)
+ return nil
+ }
+
+ if !p.CanAccess(accessMode, unitType) {
ctx.PlainText(http.StatusForbidden, "User permission denied")
return nil
}
- if task.IsForkPullRequest {
- if accessMode > perm.AccessModeRead {
- ctx.PlainText(http.StatusForbidden, "User permission denied")
- return nil
- }
- environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
- } else {
- if accessMode > perm.AccessModeWrite {
- ctx.PlainText(http.StatusForbidden, "User permission denied")
- return nil
- }
- environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
- }
+ environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, p.AccessMode))
} 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 5ba8de3d63..0164b11f66 100644
--- a/routers/web/repo/githttp_test.go
+++ b/routers/web/repo/githttp_test.go
@@ -37,6 +37,6 @@ func TestContainsParentDirectorySeparator(t *testing.T) {
}
for i := range tests {
- assert.EqualValues(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
+ assert.Equal(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
}
}
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index 6fa7579231..3401f2f666 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -7,9 +7,9 @@ import (
"net/url"
"slices"
- "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/services/context"
)
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
@@ -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/helper_test.go b/routers/web/repo/helper_test.go
index 844ad5bf79..2607fd32f8 100644
--- a/routers/web/repo/helper_test.go
+++ b/routers/web/repo/helper_test.go
@@ -6,7 +6,7 @@ package repo
import (
"testing"
- "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/user"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index e45abd3952..f445ce4a1a 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -20,44 +20,44 @@ import (
"strings"
"time"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- project_model "code.gitea.io/gitea/models/project"
- pull_model "code.gitea.io/gitea/models/pull"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/emoji"
- "code.gitea.io/gitea/modules/git"
- issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
- issue_template "code.gitea.io/gitea/modules/issue/template"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/optional"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/templates/vars"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- "code.gitea.io/gitea/services/convert"
- "code.gitea.io/gitea/services/forms"
- issue_service "code.gitea.io/gitea/services/issue"
- pull_service "code.gitea.io/gitea/services/pull"
- repo_service "code.gitea.io/gitea/services/repository"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ access_model "forgejo.org/models/perm/access"
+ project_model "forgejo.org/models/project"
+ pull_model "forgejo.org/models/pull"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/emoji"
+ "forgejo.org/modules/git"
+ issue_indexer "forgejo.org/modules/indexer/issues"
+ issue_template "forgejo.org/modules/issue/template"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/optional"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/templates"
+ "forgejo.org/modules/templates/vars"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/routers/utils"
+ asymkey_service "forgejo.org/services/asymkey"
+ "forgejo.org/services/context"
+ "forgejo.org/services/context/upload"
+ "forgejo.org/services/convert"
+ "forgejo.org/services/forms"
+ issue_service "forgejo.org/services/issue"
+ pull_service "forgejo.org/services/pull"
+ repo_service "forgejo.org/services/repository"
"code.forgejo.org/go-chi/binding"
)
@@ -76,34 +76,27 @@ const (
issueTemplateTitleKey = "IssueTemplateTitle"
)
-// 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",
+// 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
}
+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
@@ -187,9 +180,10 @@ 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")
- if selectLabels == "" {
+ switch selectLabels {
+ case "":
ctx.Data["AllLabels"] = true
- } else if selectLabels == "0" {
+ case "0":
ctx.Data["NoLabel"] = true
}
if len(selectLabels) > 0 {
@@ -347,11 +341,6 @@ 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)
@@ -426,9 +415,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- if typ == "reject" {
+ switch typ {
+ case "reject":
reviewTyp = issues_model.ReviewTypeReject
- } else if typ == "waiting" {
+ case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
@@ -1008,7 +998,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
}
@@ -1311,7 +1301,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.IsGhost() || poster.IsActions() || poster.IsAPActor() {
+ if poster.IsSystem() || poster.IsAPServerActor() {
return roleDescriptor, nil
}
@@ -1475,6 +1465,7 @@ 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")
@@ -1599,6 +1590,7 @@ 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
)
@@ -1654,15 +1646,17 @@ func ViewIssue(ctx *context.Context) {
return
}
- for _, comment = range issue.Comments {
+ for commentIdx, 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: ctx.Repo.Repository.ComposeMetas(ctx),
+ Metas: metas,
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content)
@@ -1695,7 +1689,7 @@ func ViewIssue(ctx *context.Context) {
return
}
ghostMilestone := &issues_model.Milestone{
- ID: -1,
+ ID: issues_model.GhostMilestoneID,
Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
}
if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
@@ -1739,7 +1733,7 @@ func ViewIssue(ctx *context.Context) {
Links: markup.Links{
Base: ctx.Repo.RepoLink,
},
- Metas: ctx.Repo.Repository.ComposeMetas(ctx),
+ Metas: metas,
GitRepo: ctx.Repo.GitRepo,
Ctx: ctx,
}, comment.Content)
@@ -1796,15 +1790,6 @@ 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 {
@@ -2128,7 +2113,7 @@ func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.Depende
}
repoPerms[blocker.RepoID] = perm
}
- if perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
+ if perm.CanReadIssuesOrPulls(blocker.IsPull) {
canRead = append(canRead, blocker)
} else {
notPermitted = append(notPermitted, blocker)
@@ -2412,10 +2397,6 @@ func UpdateIssueMilestone(ctx *context.Context) {
}
if ctx.FormBool("htmx") {
- renderMilestones(ctx)
- if ctx.Written() {
- return
- }
prepareHiddenCommentType(ctx)
if ctx.Written() {
return
@@ -2429,6 +2410,7 @@ func UpdateIssueMilestone(ctx *context.Context) {
ctx.ServerError("GetMilestoneByRepoID", err)
return
}
+ ctx.Data["OpenMilestones"] = true
} else {
issue.Milestone = nil
}
@@ -2786,7 +2768,7 @@ func SearchIssues(ctx *context.Context) {
IncludedAnyLabelIDs: includedAnyLabels,
MilestoneIDs: includedMilestones,
ProjectID: projectID,
- SortBy: issue_indexer.SortByCreatedDesc,
+ SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc),
}
if since != 0 {
@@ -2815,9 +2797,10 @@ func SearchIssues(ctx *context.Context) {
}
}
- // 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")
+ priorityRepoID := ctx.FormInt64("priority_repo_id")
+ if priorityRepoID > 0 {
+ searchOpt.PriorityRepoID = optional.Some(priorityRepoID)
+ }
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
if err != nil {
@@ -2955,7 +2938,7 @@ func ListIssues(ctx *context.Context) {
IsPull: isPull,
IsClosed: isClosed,
ProjectID: projectID,
- SortBy: issue_indexer.SortByCreatedDesc,
+ SortBy: issue_indexer.ParseSortBy(ctx.FormString("sort"), issue_indexer.SortByCreatedDesc),
}
if since != 0 {
searchOpt.UpdatedAfterUnix = optional.Some(since)
@@ -3117,7 +3100,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
@@ -3253,11 +3236,7 @@ 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) {
- if issue.IsPull {
- ctx.JSONError(ctx.Tr("repo.pulls.comment.blocked_by_user"))
- } else {
- ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_by_user"))
- }
+ ctx.JSONError(ctx.Tr("repo.comment.blocked_by_user"))
} else {
ctx.ServerError("CreateIssueComment", err)
}
@@ -3607,9 +3586,9 @@ func GetIssueAttachments(ctx *context.Context) {
if ctx.Written() {
return
}
- attachments := make([]*api.Attachment, len(issue.Attachments))
+ attachments := make([]*api.WebAttachment, len(issue.Attachments))
for i := 0; i < len(issue.Attachments); i++ {
- attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i])
+ attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, issue.Attachments[i])
}
ctx.JSON(http.StatusOK, attachments)
}
@@ -3632,7 +3611,7 @@ func GetCommentAttachments(ctx *context.Context) {
return
}
- if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
+ if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{})
return
}
@@ -3642,13 +3621,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 = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i]))
+ attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, comment.Attachments[i])
}
ctx.JSON(http.StatusOK, attachments)
}
@@ -3798,3 +3777,49 @@ 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 4ce76b2bb9..11d0de90de 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -9,12 +9,12 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models/avatars"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/avatars"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/templates"
+ "forgejo.org/services/context"
"github.com/sergi/go-diff/diffmatchpatch"
)
@@ -160,15 +160,16 @@ func GetContentHistoryDetail(ctx *context.Context) {
diffHTMLBuf := bytes.Buffer{}
diffHTMLBuf.WriteString("")
for _, it := range diff {
- if it.Type == diffmatchpatch.DiffInsert {
+ switch it.Type {
+ case diffmatchpatch.DiffInsert:
diffHTMLBuf.WriteString("")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString(" ")
- } else if it.Type == diffmatchpatch.DiffDelete {
+ case diffmatchpatch.DiffDelete:
diffHTMLBuf.WriteString("")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString(" ")
- } else {
+ default:
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
}
}
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index 66b38688ec..3764a6bd7e 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
// AddDependency adds new dependencies
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 81bee4dbb5..74674e9550 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -6,17 +6,17 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/label"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- issue_service "code.gitea.io/gitea/services/issue"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/label"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ issue_service "forgejo.org/services/issue"
)
const (
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 2b4915e855..0adcc39499 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -8,13 +8,13 @@ import (
"strconv"
"testing"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/contexttest"
- "code.gitea.io/gitea/services/forms"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/test"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/contexttest"
+ "forgejo.org/services/forms"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -39,7 +39,7 @@ func TestInitializeLabels(t *testing.T) {
contexttest.LoadRepo(t, ctx, 2)
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, testCase.ExpectedLabelIDs[i], label.ID)
+ assert.Equal(t, testCase.ExpectedLabelIDs[i], label.ID)
}
}
}
@@ -85,7 +85,7 @@ func TestNewLabel(t *testing.T) {
Color: "#abcdef",
})
NewLabel(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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/issue_lock.go b/routers/web/repo/issue_lock.go
index 1d5fc8a5f3..dea67ab996 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -4,10 +4,10 @@
package repo
import (
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
// LockIssue locks an issue. This would limit commenting abilities to
diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go
index 365c812681..5e2075a17f 100644
--- a/routers/web/repo/issue_pin.go
+++ b/routers/web/repo/issue_pin.go
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/services/context"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/services/context"
)
// IssuePinOrUnpin pin or unpin a Issue
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index 70d42b27c0..5bc49464dd 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -7,10 +7,10 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/eventsource"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/eventsource"
+ "forgejo.org/services/context"
)
// IssueStopwatch creates or stops a stopwatch for the given issue.
diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
index 241e434049..e63f7e2dc2 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -7,12 +7,12 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
// AddTimeManually tracks time manually
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index 5cff9f4ddd..5af223f865 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -7,10 +7,10 @@ import (
"net/http"
"strconv"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/services/context"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/main_test.go b/routers/web/repo/main_test.go
index 6e469cf2ed..8b30ad41ed 100644
--- a/routers/web/repo/main_test.go
+++ b/routers/web/repo/main_test.go
@@ -6,7 +6,7 @@ package repo
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/routers/web/repo/middlewares.go b/routers/web/repo/middlewares.go
index ddda9f3ff2..9aba447433 100644
--- a/routers/web/repo/middlewares.go
+++ b/routers/web/repo/middlewares.go
@@ -7,12 +7,12 @@ import (
"fmt"
"strconv"
- system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/services/context"
- user_service "code.gitea.io/gitea/services/user"
+ system_model "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/optional"
+ "forgejo.org/services/context"
+ user_service "forgejo.org/services/user"
)
// SetEditorconfigIfExists set editor config as render variable
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index 0acf966bca..3a5cf30dbe 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -9,23 +9,23 @@ import (
"net/url"
"strings"
- "code.gitea.io/gitea/models"
- admin_model "code.gitea.io/gitea/models/admin"
- "code.gitea.io/gitea/models/db"
- quota_model "code.gitea.io/gitea/models/quota"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/migrations"
- "code.gitea.io/gitea/services/task"
+ "forgejo.org/models"
+ admin_model "forgejo.org/models/admin"
+ "forgejo.org/models/db"
+ quota_model "forgejo.org/models/quota"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/migrations"
+ "forgejo.org/services/task"
)
const (
@@ -138,6 +138,8 @@ 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/milestone.go b/routers/web/repo/milestone.go
index 1c53f73fdb..920a9ee12a 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -9,18 +9,18 @@ import (
"net/url"
"time"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/issue"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/issue"
"xorm.io/builder"
)
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index 11874ab0d0..c947fb99bf 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -6,13 +6,13 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/db"
+ "forgejo.org/models/packages"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
index d234f6c964..688ef19375 100644
--- a/routers/web/repo/patch.go
+++ b/routers/web/repo/patch.go
@@ -6,16 +6,16 @@ package repo
import (
"strings"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/repository/files"
)
const (
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 0689b0a721..e5bd06e987 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -9,22 +9,22 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/perm"
- project_model "code.gitea.io/gitea/models/project"
- attachment_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/perm"
+ project_model "forgejo.org/models/project"
+ attachment_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
const (
@@ -120,7 +120,7 @@ func Projects(ctx *context.Context) {
pager.AddParam(ctx, "state", "State")
ctx.Data["Page"] = pager
- ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.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.Permission.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.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.Permission.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.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.Permission.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.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.Permission.CanWrite(unit.TypeProjects)
+ ctx.Data["CanWriteProjects"] = ctx.Repo.CanWrite(unit.TypeProjects)
ctx.Data["Project"] = project
ctx.Data["IssuesMap"] = issuesMap
ctx.Data["Columns"] = columns
diff --git a/routers/web/repo/projects_test.go b/routers/web/repo/projects_test.go
index d61230a57e..bc8b747980 100644
--- a/routers/web/repo/projects_test.go
+++ b/routers/web/repo/projects_test.go
@@ -6,8 +6,8 @@ package repo
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/services/contexttest"
+ "forgejo.org/models/unittest"
+ "forgejo.org/services/contexttest"
"github.com/stretchr/testify/assert"
)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 98dacc1a0d..e484187b54 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -10,45 +10,49 @@ import (
"errors"
"fmt"
"html"
+ "html/template"
"net/http"
"net/url"
+ "path"
"strconv"
"strings"
- "time"
- "code.gitea.io/gitea/models"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- pull_model "code.gitea.io/gitea/models/pull"
- quota_model "code.gitea.io/gitea/models/quota"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/emoji"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- issue_template "code.gitea.io/gitea/modules/issue/template"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- "code.gitea.io/gitea/services/automerge"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/gitdiff"
- notify_service "code.gitea.io/gitea/services/notify"
- pull_service "code.gitea.io/gitea/services/pull"
- repo_service "code.gitea.io/gitea/services/repository"
+ "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"
+ "forgejo.org/models/organization"
+ access_model "forgejo.org/models/perm/access"
+ pull_model "forgejo.org/models/pull"
+ quota_model "forgejo.org/models/quota"
+ repo_model "forgejo.org/models/repo"
+ "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"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/routers/utils"
+ asymkey_service "forgejo.org/services/asymkey"
+ "forgejo.org/services/automerge"
+ "forgejo.org/services/context"
+ "forgejo.org/services/context/upload"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/gitdiff"
+ notify_service "forgejo.org/services/notify"
+ pull_service "forgejo.org/services/pull"
+ repo_service "forgejo.org/services/repository"
"github.com/gobwas/glob"
)
@@ -62,33 +66,27 @@ const (
pullRequestTemplateKey = "PullRequestTemplate"
)
-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",
+// 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 = generatePullRequestTemplateLocations()
+
func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
@@ -356,7 +354,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
ctx.Data["Issue"] = issue
if !issue.IsPull {
- ctx.NotFound("ViewPullCommits", nil)
+ ctx.Redirect(issue.Link())
return nil, false
}
@@ -401,6 +399,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
// GetPullDiffStats get Pull Requests diff stats
func GetPullDiffStats(ctx *context.Context) {
+ // FIXME: this getPullInfo seems to be a duplicate call with other route handlers
issue, ok := getPullInfo(ctx)
if !ok {
return
@@ -408,15 +407,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 {
- ctx.ServerError("GetRefCommitID", err)
+ log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
@@ -498,6 +497,7 @@ 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,6 +508,12 @@ 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)
@@ -515,9 +521,6 @@ 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
@@ -581,9 +584,6 @@ 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,6 +597,7 @@ 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
}
@@ -607,6 +608,13 @@ 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
}
@@ -624,7 +632,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 = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName())
+ headBranchExist = baseGitRepo.IsReferenceExist(pull.GetGitRefName())
}
if headBranchExist {
@@ -665,6 +673,7 @@ 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
}
@@ -677,9 +686,6 @@ 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
@@ -727,7 +733,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 && (headBranchSha != sha)) {
+ if pull.HeadRepo == nil || !headBranchExist || (!pull.Issue.IsClosed && !pull.IsChecking() && (headBranchSha != sha)) {
ctx.Data["IsPullRequestBroken"] = true
if pull.IsSameRepo() {
ctx.Data["HeadTarget"] = pull.HeadBranch
@@ -745,6 +751,7 @@ 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
}
@@ -769,6 +776,13 @@ 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
}
@@ -847,7 +861,7 @@ func ViewPullCommits(ctx *context.Context) {
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- commits := processGitCommits(ctx, prInfo.Commits)
+ commits := git_model.ParseCommitsWithStatus(ctx, prInfo.Commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
@@ -892,7 +906,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
@@ -907,7 +921,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
}
@@ -928,7 +942,85 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit
- if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
+ 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 len(specifiedEndCommit) > 0 {
endCommitID = specifiedEndCommit
} else {
@@ -939,6 +1031,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
} else {
startCommitID = prInfo.MergeBase
}
+
ctx.Data["IsShowingAllCommits"] = false
} else {
endCommitID = headCommitID
@@ -946,10 +1039,10 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
ctx.Data["IsShowingAllCommits"] = true
}
- ctx.Data["Username"] = ctx.Repo.Owner.Name
- ctx.Data["Reponame"] = ctx.Repo.Repository.Name
ctx.Data["AfterCommitID"] = endCommitID
ctx.Data["BeforeCommitID"] = startCommitID
+ ctx.Data["Username"] = ctx.Repo.Owner.Name
+ ctx.Data["Reponame"] = ctx.Repo.Repository.Name
fileOnly := ctx.FormBool("file-only")
@@ -981,7 +1074,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.GetDiff(ctx, gitRepo, diffOptions, files...)
+ diff, err = gitdiff.GetDiffFull(ctx, gitRepo, diffOptions, files...)
methodWithError = "GetDiff"
} else {
diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...)
@@ -1053,7 +1146,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)
+ ctx.Data["HeadBranchIsEditable"] = pull.HeadRepo.CanEnableEditor() && issues_model.CanMaintainerWriteToBranch(ctx, headRepoPerm, pull.HeadBranch, ctx.Doer) && pull.Flow != issues_model.PullRequestFlowAGit
ctx.Data["SourceRepoLink"] = pull.HeadRepo.Link()
ctx.Data["HeadBranch"] = pull.HeadBranch
}
@@ -1074,6 +1167,13 @@ 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
@@ -1207,8 +1307,6 @@ 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())
}
@@ -1321,8 +1419,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.editor.merge_conflict"),
- "Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
+ "Message": ctx.Tr("repo.pulls.merge_conflict"),
+ "Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + " " + utils.SanitizeFlashErrorString(conflictError.StdOut),
})
if err != nil {
@@ -1348,6 +1446,10 @@ 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 eb8dd83d9c..941e428039 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -8,17 +8,17 @@ import (
"fmt"
"net/http"
- issues_model "code.gitea.io/gitea/models/issues"
- pull_model "code.gitea.io/gitea/models/pull"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- "code.gitea.io/gitea/services/forms"
- pull_service "code.gitea.io/gitea/services/pull"
+ issues_model "forgejo.org/models/issues"
+ pull_model "forgejo.org/models/pull"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/context/upload"
+ "forgejo.org/services/forms"
+ pull_service "forgejo.org/services/pull"
)
const (
@@ -211,9 +211,10 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
- if origin == "diff" {
+ switch origin {
+ case "diff":
ctx.HTML(http.StatusOK, tplDiffConversation)
- } else if origin == "timeline" {
+ case "timeline":
ctx.HTML(http.StatusOK, tplTimelineConversation)
}
}
diff --git a/routers/web/repo/pull_review_test.go b/routers/web/repo/pull_review_test.go
index 329e83fe4b..14e6714a63 100644
--- a/routers/web/repo/pull_review_test.go
+++ b/routers/web/repo/pull_review_test.go
@@ -8,13 +8,13 @@ import (
"net/http/httptest"
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/contexttest"
- "code.gitea.io/gitea/services/pull"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/templates"
+ "forgejo.org/services/context"
+ "forgejo.org/services/contexttest"
+ "forgejo.org/services/pull"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
index c158fb30b6..211b1b2b12 100644
--- a/routers/web/repo/recent_commits.go
+++ b/routers/web/repo/recent_commits.go
@@ -4,12 +4,10 @@
package repo
import (
- "errors"
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/services/context"
- contributors_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/modules/base"
+ "forgejo.org/services/context"
)
const (
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRecentCommits)
}
-
-// RecentCommitsData returns JSON of recent commits data
-func RecentCommitsData(ctx *context.Context) {
- if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.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 1791788743..a6de337192 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -1,5 +1,6 @@
// 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
@@ -10,29 +11,29 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/web/feed"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/context/upload"
- "code.gitea.io/gitea/services/forms"
- releaseservice "code.gitea.io/gitea/services/release"
+ "forgejo.org/models"
+ "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/routers/web/feed"
+ "forgejo.org/services/context"
+ "forgejo.org/services/context/upload"
+ "forgejo.org/services/forms"
+ releaseservice "forgejo.org/services/release"
)
const (
@@ -249,7 +250,7 @@ func addVerifyTagToContext(ctx *context.Context) {
if verification == nil {
return false
}
- return verification.Reason != "gpg.error.not_signed_commit"
+ return verification.Reason != asymkey.NotSigned
}
}
diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go
index 5c7b6e2e8f..785b1fdf69 100644
--- a/routers/web/repo/release_test.go
+++ b/routers/web/repo/release_test.go
@@ -6,13 +6,13 @@ package repo
import (
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/contexttest"
- "code.gitea.io/gitea/services/forms"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/contexttest"
+ "forgejo.org/services/forms"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index e64db03e20..05eeadc519 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -9,13 +9,13 @@ import (
"net/http"
"path"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/typesniffer"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/context"
)
// RenderFile renders a file by repos path
@@ -41,7 +41,7 @@ func RenderFile(ctx *context.Context) {
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
- st := typesniffer.DetectContentType(buf)
+ st := typesniffer.DetectContentType(buf, blob.Name())
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 2e8ca61bf5..493787ad8b 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -12,32 +12,32 @@ import (
"slices"
"strings"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- quota_model "code.gitea.io/gitea/models/quota"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
- "code.gitea.io/gitea/services/forms"
- repo_service "code.gitea.io/gitea/services/repository"
- archiver_service "code.gitea.io/gitea/services/repository/archiver"
- commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/models/organization"
+ access_model "forgejo.org/models/perm/access"
+ quota_model "forgejo.org/models/quota"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/convert"
+ "forgejo.org/services/forms"
+ repo_service "forgejo.org/services/repository"
+ archiver_service "forgejo.org/services/repository/archiver"
+ commitstatus_service "forgejo.org/services/repository/commitstatus"
)
const (
@@ -693,9 +693,6 @@ 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 {
@@ -782,3 +779,27 @@ 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 d10eb67528..ad10542c01 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -7,12 +7,12 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- code_indexer "code.gitea.io/gitea/modules/indexer/code"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ code_indexer "forgejo.org/modules/indexer/code"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
const tplSearch base.TplName = "repo/search"
@@ -165,6 +165,8 @@ 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 504f57cfc2..20e211316d 100644
--- a/routers/web/repo/setting/avatar.go
+++ b/routers/web/repo/setting/avatar.go
@@ -8,13 +8,13 @@ import (
"fmt"
"io"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/typesniffer"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ repo_service "forgejo.org/services/repository"
)
// UpdateAvatarSetting update repo's avatar
@@ -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/collaboration.go b/routers/web/repo/setting/collaboration.go
index 75b55151e7..a816a16bc8 100644
--- a/routers/web/repo/setting/collaboration.go
+++ b/routers/web/repo/setting/collaboration.go
@@ -8,19 +8,19 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/mailer"
- org_service "code.gitea.io/gitea/services/org"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
+ "forgejo.org/services/mailer"
+ org_service "forgejo.org/services/org"
+ repo_service "forgejo.org/services/repository"
)
// Collaboration render a repository's collaboration page
diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go
index 881d148afc..1c6033f1e4 100644
--- a/routers/web/repo/setting/default_branch.go
+++ b/routers/web/repo/setting/default_branch.go
@@ -6,12 +6,12 @@ package setting
import (
"net/http"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/routers/web/repo"
- "code.gitea.io/gitea/services/context"
- repo_service "code.gitea.io/gitea/services/repository"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/routers/web/repo"
+ "forgejo.org/services/context"
+ repo_service "forgejo.org/services/repository"
)
// SetDefaultBranchPost set default branch
diff --git a/routers/web/repo/setting/deploy_key.go b/routers/web/repo/setting/deploy_key.go
index abc3eb4af1..c59f0e90c2 100644
--- a/routers/web/repo/setting/deploy_key.go
+++ b/routers/web/repo/setting/deploy_key.go
@@ -6,14 +6,14 @@ package setting
import (
"net/http"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ asymkey_service "forgejo.org/services/asymkey"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
// DeployKeys render the deploy keys list of a repository page
diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go
index 217a01c90c..a50bce2a27 100644
--- a/routers/web/repo/setting/git_hooks.go
+++ b/routers/web/repo/setting/git_hooks.go
@@ -6,8 +6,8 @@ package setting
import (
"net/http"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/git"
+ "forgejo.org/services/context"
)
// GitHooks hooks of a repository
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 7e3634375a..9930d03e8e 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -14,20 +14,20 @@ import (
"strconv"
"strings"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/git/pipeline"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/context"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/git/pipeline"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/typesniffer"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/context"
)
const (
@@ -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,6 +342,20 @@ 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/main_test.go b/routers/web/repo/setting/main_test.go
index c414b853e5..6b5a70ba08 100644
--- a/routers/web/repo/setting/main_test.go
+++ b/routers/web/repo/setting/main_test.go
@@ -6,7 +6,7 @@ package setting
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index b2f5798a26..18efbc37c4 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -11,17 +11,17 @@ import (
"strings"
"time"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/web/repo"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- pull_service "code.gitea.io/gitea/services/pull"
- "code.gitea.io/gitea/services/repository"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/web"
+ "forgejo.org/routers/web/repo"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ pull_service "forgejo.org/services/pull"
+ "forgejo.org/services/repository"
"github.com/gobwas/glob"
)
diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go
index 2c25b650b9..5735149dfd 100644
--- a/routers/web/repo/setting/protected_tag.go
+++ b/routers/web/repo/setting/protected_tag.go
@@ -8,15 +8,15 @@ import (
"net/http"
"strings"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
const (
diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go
index 9dce5d13b7..32c8667825 100644
--- a/routers/web/repo/setting/runners.go
+++ b/routers/web/repo/setting/runners.go
@@ -8,13 +8,13 @@ import (
"net/http"
"net/url"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- actions_shared "code.gitea.io/gitea/routers/web/shared/actions"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ actions_shared "forgejo.org/routers/web/shared/actions"
+ shared_user "forgejo.org/routers/web/shared/user"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go
index d4d56bfc57..11c83e8bd6 100644
--- a/routers/web/repo/setting/secrets.go
+++ b/routers/web/repo/setting/secrets.go
@@ -7,11 +7,11 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- shared "code.gitea.io/gitea/routers/web/shared/secrets"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ shared "forgejo.org/routers/web/shared/secrets"
+ shared_user "forgejo.org/routers/web/shared/user"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index df7e388680..1f54ba353d 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -6,6 +6,7 @@
package setting
import (
+ go_context "context"
"errors"
"fmt"
"net/http"
@@ -13,34 +14,34 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- quota_model "code.gitea.io/gitea/models/quota"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/indexer/code"
- "code.gitea.io/gitea/modules/indexer/stats"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/modules/web"
- actions_service "code.gitea.io/gitea/services/actions"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/federation"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/migrations"
- mirror_service "code.gitea.io/gitea/services/mirror"
- repo_service "code.gitea.io/gitea/services/repository"
- wiki_service "code.gitea.io/gitea/services/wiki"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ quota_model "forgejo.org/models/quota"
+ 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/git"
+ "forgejo.org/modules/indexer/code"
+ "forgejo.org/modules/indexer/issues"
+ "forgejo.org/modules/indexer/stats"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/validation"
+ "forgejo.org/modules/web"
+ actions_service "forgejo.org/services/actions"
+ asymkey_service "forgejo.org/services/asymkey"
+ "forgejo.org/services/context"
+ "forgejo.org/services/federation"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/migrations"
+ mirror_service "forgejo.org/services/mirror"
+ repo_service "forgejo.org/services/repository"
+ wiki_service "forgejo.org/services/wiki"
)
const (
@@ -64,6 +65,9 @@ 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
@@ -105,6 +109,10 @@ func Units(ctx *context.Context) {
func UnitsPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RepoUnitSettingForm)
+ if ctx.HasError() {
+ ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/units")
+ return
+ }
repo := ctx.Repo.Repository
@@ -146,11 +154,9 @@ func UnitsPost(ctx *context.Context) {
})
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
} else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
- var wikiPermissions repo_model.UnitAccessMode
+ wikiPermissions := repo_model.UnitAccessModeUnset
if form.GloballyWriteableWiki {
wikiPermissions = repo_model.UnitAccessModeWrite
- } else {
- wikiPermissions = repo_model.UnitAccessModeRead
}
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
@@ -537,7 +543,13 @@ func SettingsPost(ctx *context.Context) {
mirror_service.AddPullMirrorToQueue(repo.ID)
- ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
+ 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.Redirect(repo.Link() + "/settings")
case "push-mirror-sync":
@@ -584,6 +596,23 @@ 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.
@@ -679,6 +708,7 @@ func SettingsPost(ctx *context.Context) {
SyncOnCommit: form.PushMirrorSyncOnCommit,
Interval: interval,
RemoteAddress: remoteAddress,
+ BranchFilter: form.PushMirrorBranchFilter,
}
var plainPrivateKey []byte
@@ -772,6 +802,8 @@ 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
@@ -796,13 +828,9 @@ func SettingsPost(ctx *context.Context) {
ctx.Error(http.StatusNotFound)
return
}
- repo.IsMirror = false
- 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)
+ if err := repo_service.ConvertMirrorToNormalRepo(ctx, ctx.Repo.Repository); err != nil {
+ ctx.ServerError("ConvertMirror", err)
return
}
log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
@@ -1030,7 +1058,7 @@ func SettingsPost(ctx *context.Context) {
return
}
- if err := actions_model.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
+ if err := actions_service.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
@@ -1084,6 +1112,8 @@ 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 0c8553faea..3a81b85e4c 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -7,41 +7,27 @@ import (
"net/http"
"testing"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/contexttest"
- "code.gitea.io/gitea/services/forms"
- repo_service "code.gitea.io/gitea/services/repository"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ repo_model "forgejo.org/models/repo"
+ "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"
+ "forgejo.org/services/forms"
+ repo_service "forgejo.org/services/repository"
"github.com/stretchr/testify/assert"
"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) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys")
@@ -55,7 +41,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -65,11 +51,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
@@ -85,7 +67,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -124,7 +106,7 @@ func TestCollaborationPost(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
require.NoError(t, err)
@@ -150,7 +132,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -184,7 +166,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
require.NoError(t, err)
@@ -193,7 +175,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
// Try adding the same collaborator again
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -215,7 +197,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -255,7 +237,7 @@ func TestAddTeamPost(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
assert.Empty(t, ctx.Flash.ErrorMsg)
}
@@ -295,7 +277,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) {
AddTeamPost(ctx)
assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -336,7 +318,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -369,7 +351,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) {
ctx.Repo = repo
AddTeamPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
index 4fb8c06e84..a83d2dea6f 100644
--- a/routers/web/repo/setting/variables.go
+++ b/routers/web/repo/setting/variables.go
@@ -7,11 +7,11 @@ import (
"errors"
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- shared "code.gitea.io/gitea/routers/web/shared/actions"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ shared "forgejo.org/routers/web/shared/actions"
+ shared_user "forgejo.org/routers/web/shared/user"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index af54997794..0caa196e25 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -11,22 +11,22 @@ import (
"net/url"
"path"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/web/middleware"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
- "code.gitea.io/gitea/services/forms"
- webhook_service "code.gitea.io/gitea/services/webhook"
+ "forgejo.org/models/db"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/models/webhook"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/web/middleware"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/context"
+ "forgejo.org/services/convert"
+ "forgejo.org/services/forms"
+ webhook_service "forgejo.org/services/webhook"
"code.forgejo.org/go-chi/binding"
)
@@ -175,6 +175,9 @@ 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/topic.go b/routers/web/repo/topic.go
index d81a695df9..a028afb042 100644
--- a/routers/web/repo/topic.go
+++ b/routers/web/repo/topic.go
@@ -7,9 +7,9 @@ import (
"net/http"
"strings"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/services/context"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/log"
+ "forgejo.org/services/context"
)
// TopicsPost response for creating repository
diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go
index d11af4669f..20ea9babbe 100644
--- a/routers/web/repo/treelist.go
+++ b/routers/web/repo/treelist.go
@@ -6,9 +6,9 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ "forgejo.org/services/context"
"github.com/go-enry/go-enry/v2"
)
@@ -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 9030b03a90..3b11d73390 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -1,5 +1,6 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
@@ -24,37 +25,38 @@ import (
_ "image/jpeg" // for processing jpeg images
_ "image/png" // for processing png images
- activities_model "code.gitea.io/gitea/models/activities"
- admin_model "code.gitea.io/gitea/models/admin"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issue_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/highlight"
- code_indexer "code.gitea.io/gitea/modules/indexer/code"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/svg"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/routers/web/feed"
- "code.gitea.io/gitea/services/context"
- issue_service "code.gitea.io/gitea/services/issue"
- files_service "code.gitea.io/gitea/services/repository/files"
+ activities_model "forgejo.org/models/activities"
+ admin_model "forgejo.org/models/admin"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issue_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/actions"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/highlight"
+ code_indexer "forgejo.org/modules/indexer/code"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/svg"
+ "forgejo.org/modules/typesniffer"
+ "forgejo.org/modules/util"
+ "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"
- "github.com/nektos/act/pkg/model"
+ "code.forgejo.org/forgejo/runner/v11/act/model"
_ "golang.org/x/image/bmp" // for processing bmp images
_ "golang.org/x/image/webp" // for processing webp images
@@ -226,7 +228,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
- st := typesniffer.DetectContentType(buf)
+ st := typesniffer.DetectContentType(buf, blob.Name())
isTextFile := st.IsText()
// FIXME: what happens when README file is an image?
@@ -260,7 +262,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
}
buf = buf[:n]
- st = typesniffer.DetectContentType(buf)
+ st = typesniffer.DetectContentType(buf, blob.Name())
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
}
@@ -367,9 +369,6 @@ 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
@@ -435,13 +434,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
if err != nil {
log.Error("actions.GetContentFromEntry: %v", err)
}
- _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content))
+ _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content), true)
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"}, ctx.Repo.TreePath) {
- if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
- _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
+ } 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)
if len(warnings) > 0 {
ctx.Data["FileWarning"] = strings.Join(warnings, "\n")
}
@@ -625,6 +624,20 @@ 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
@@ -1044,7 +1057,14 @@ func renderHomeCode(ctx *context.Context) {
return
}
- if entry.IsDir() {
+ if entry.IsSubmodule() {
+ submodule, err := ctx.Repo.Commit.GetSubmodule(ctx.Repo.TreePath, entry)
+ if err != nil {
+ HandleGitError(ctx, "Repo.Commit.GetSubmodule", err)
+ return
+ }
+ ctx.Redirect(submodule.ResolveUpstreamURL(ctx.Repo.Repository.HTMLURL()))
+ } else if entry.IsDir() {
renderDirectory(ctx)
} else {
renderFile(ctx, entry)
@@ -1146,6 +1166,20 @@ 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()
@@ -1187,7 +1221,7 @@ func checkOutdatedBranch(ctx *context.Context) {
}
if dbBranch.CommitID != commit.ID.String() {
- ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true)
+ ctx.Flash.Warning(ctx.Tr("warning.repository.out_of_sync"), true)
}
}
@@ -1217,6 +1251,7 @@ 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) {
@@ -1228,6 +1263,7 @@ 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 070d07cdf3..1b5265978a 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -14,25 +14,25 @@ import (
"path/filepath"
"strings"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/common"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- notify_service "code.gitea.io/gitea/services/notify"
- wiki_service "code.gitea.io/gitea/services/wiki"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/routers/common"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ notify_service "forgejo.org/services/notify"
+ wiki_service "forgejo.org/services/wiki"
)
const (
@@ -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.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
+ ctx.Data["Commits"] = git_model.ParseCommitsWithStatus(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 0c49e7d902..5709b32257 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -9,14 +9,14 @@ import (
"net/url"
"testing"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/contexttest"
- "code.gitea.io/gitea/services/forms"
- wiki_service "code.gitea.io/gitea/services/wiki"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/contexttest"
+ "forgejo.org/services/forms"
+ wiki_service "forgejo.org/services/wiki"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -73,7 +73,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) {
return
}
for i, pageMeta := range pageMetas {
- assert.EqualValues(t, expectedNames[i], pageMeta.Name)
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath)
+ assert.Equal(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath)
} else {
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath)
- assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
+ assert.Equal(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath)
+ assert.Equal(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
new file mode 100644
index 0000000000..d783f83110
--- /dev/null
+++ b/routers/web/shared/actions/fixtures/TestRunnerDetails/action_runner.yml
@@ -0,0 +1,7 @@
+-
+ 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
new file mode 100644
index 0000000000..63a2d30deb
--- /dev/null
+++ b/routers/web/shared/actions/fixtures/TestRunnerDetails/action_task.yml
@@ -0,0 +1,160 @@
+-
+ 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
new file mode 100644
index 0000000000..056f48b98d
--- /dev/null
+++ b/routers/web/shared/actions/main_test.go
@@ -0,0 +1,17 @@
+// 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 66dce1412b..2ab6b2dadd 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -6,13 +6,13 @@ package actions
import (
"errors"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
// RunnersList prepares data for runners list
@@ -79,7 +79,6 @@ 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
new file mode 100644
index 0000000000..ad75d34ee6
--- /dev/null
+++ b/routers/web/shared/actions/runners_test.go
@@ -0,0 +1,47 @@
+// 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/actions/variables.go b/routers/web/shared/actions/variables.go
index 47f1176f46..13dff2f11a 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -4,13 +4,13 @@
package actions
import (
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/web"
- actions_service "code.gitea.io/gitea/services/actions"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/web"
+ actions_service "forgejo.org/services/actions"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index af960f1c0c..538c1e4b71 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -9,19 +9,18 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models/db"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- cargo_service "code.gitea.io/gitea/services/packages/cargo"
- container_service "code.gitea.io/gitea/services/packages/container"
+ packages_model "forgejo.org/models/packages"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ cargo_service "forgejo.org/services/packages/cargo"
+ container_service "forgejo.org/services/packages/container"
)
func SetPackagesContext(ctx *context.Context, owner *user_model.User) {
@@ -168,13 +167,12 @@ 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 {
+ for _, pv := range pvs[pcr.KeepCount:] {
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
ctx.ServerError("ShouldBeSkipped", err)
return
diff --git a/routers/web/shared/project/column.go b/routers/web/shared/project/column.go
index 599842ea9e..40bb439452 100644
--- a/routers/web/shared/project/column.go
+++ b/routers/web/shared/project/column.go
@@ -4,9 +4,9 @@
package project
import (
- project_model "code.gitea.io/gitea/models/project"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/services/context"
+ project_model "forgejo.org/models/project"
+ "forgejo.org/modules/json"
+ "forgejo.org/services/context"
)
// MoveColumns moves or keeps columns in a project and sorts them inside that project
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index 3bd421f86a..a853598939 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -4,14 +4,14 @@
package secrets
import (
- "code.gitea.io/gitea/models/db"
- secret_model "code.gitea.io/gitea/models/secret"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- secret_service "code.gitea.io/gitea/services/secrets"
+ "forgejo.org/models/db"
+ secret_model "forgejo.org/models/secret"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ secret_service "forgejo.org/services/secrets"
)
func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
diff --git a/routers/web/shared/storage_overview.go b/routers/web/shared/storage_overview.go
index 3bebdfb688..fac4aa99e5 100644
--- a/routers/web/shared/storage_overview.go
+++ b/routers/web/shared/storage_overview.go
@@ -7,10 +7,10 @@ import (
"html/template"
"net/http"
- quota_model "code.gitea.io/gitea/models/quota"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ quota_model "forgejo.org/models/quota"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
// StorageOverview render a size overview of the user, as well as relevant
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index fd7605c33b..87feb966cb 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -7,22 +7,23 @@ package user
import (
"net/url"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- packages_model "code.gitea.io/gitea/models/packages"
- access_model "code.gitea.io/gitea/models/perm/access"
- project_model "code.gitea.io/gitea/models/project"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ packages_model "forgejo.org/models/packages"
+ access_model "forgejo.org/models/perm/access"
+ project_model "forgejo.org/models/project"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/routers/web/repo"
+ "forgejo.org/services/context"
)
// prepareContextForCommonProfile store some common data into context data for user's profile related pages (including the nav menu)
@@ -38,6 +39,7 @@ 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
@@ -66,6 +68,7 @@ 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 {
@@ -102,7 +105,22 @@ 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 {
- profileReadmeBlob, _ = commit.GetBlobByFoldedPath("README.md")
+ 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()
+ }
+ }
+ }
}
}
}
diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go
index fc39b504a9..1569600734 100644
--- a/routers/web/swagger_json.go
+++ b/routers/web/swagger_json.go
@@ -4,7 +4,7 @@
package web
import (
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/services/context"
)
// SwaggerV1Json render swagger v1 json
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index 04f510161d..76cc342770 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -7,10 +7,10 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/avatars"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/httpcache"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/avatars"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/httpcache"
+ "forgejo.org/services/context"
)
func cacheableRedirect(ctx *context.Context, location string) {
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index 019249e3e0..b5c5e54953 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -6,13 +6,13 @@ package user
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/base"
- code_indexer "code.gitea.io/gitea/modules/indexer/code"
- "code.gitea.io/gitea/modules/setting"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/base"
+ code_indexer "forgejo.org/modules/indexer/code"
+ "forgejo.org/modules/setting"
+ shared_user "forgejo.org/routers/web/shared/user"
+ "forgejo.org/services/context"
)
const (
@@ -131,6 +131,7 @@ 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 a0841c0227..d980fa393a 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -13,27 +13,26 @@ import (
"strconv"
"strings"
- activities_model "code.gitea.io/gitea/models/activities"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/routers/web/feed"
- "code.gitea.io/gitea/services/context"
- issue_service "code.gitea.io/gitea/services/issue"
- pull_service "code.gitea.io/gitea/services/pull"
+ activities_model "forgejo.org/models/activities"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/container"
+ issue_indexer "forgejo.org/modules/indexer/issues"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/routers/web/feed"
+ "forgejo.org/services/context"
+ issue_service "forgejo.org/services/issue"
+ pull_service "forgejo.org/services/pull"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
@@ -611,11 +610,6 @@ 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.
@@ -655,9 +649,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- if typ == "reject" {
+ switch typ {
+ case "reject":
reviewTyp = issues_model.ReviewTypeReject
- } else if typ == "waiting" {
+ case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index c09f609161..f3a2f12ae6 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -7,14 +7,14 @@ import (
"net/http"
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/contexttest"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/templates"
+ "forgejo.org/services/context"
+ "forgejo.org/services/contexttest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -40,15 +40,15 @@ func TestArchivedIssues(t *testing.T) {
NumIssues[repo.ID] = repo.NumIssues
}
assert.False(t, IsArchived[50])
- assert.EqualValues(t, 1, NumIssues[50])
+ assert.Equal(t, 1, NumIssues[50])
assert.True(t, IsArchived[51])
- assert.EqualValues(t, 1, NumIssues[51])
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.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())
+ 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())
}
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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(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.EqualValues(t, orgLabels[i].OrgID, label.OrgID)
- assert.EqualValues(t, orgLabels[i].ID, label.ID)
- assert.EqualValues(t, orgLabels[i].Name, label.Name)
+ assert.Equal(t, orgLabels[i].OrgID, label.OrgID)
+ assert.Equal(t, orgLabels[i].ID, label.ID)
+ assert.Equal(t, orgLabels[i].Name, label.Name)
}
}
}
diff --git a/routers/web/user/main_test.go b/routers/web/user/main_test.go
index 8b6ae69296..080e3fdcfe 100644
--- a/routers/web/user/main_test.go
+++ b/routers/web/user/main_test.go
@@ -6,7 +6,7 @@ package user
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index c3358dbf62..fdca1a2fdd 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -11,21 +11,19 @@ import (
"net/url"
"strings"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/context"
- issue_service "code.gitea.io/gitea/services/issue"
- pull_service "code.gitea.io/gitea/services/pull"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/context"
+ issue_service "forgejo.org/services/issue"
+ pull_service "forgejo.org/services/pull"
)
const (
@@ -311,11 +309,6 @@ 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
@@ -340,9 +333,10 @@ func NotificationSubscriptions(ctx *context.Context) {
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- if typ == "reject" {
+ switch typ {
+ case "reject":
reviewTyp = issues_model.ReviewTypeReject
- } else if typ == "waiting" {
+ case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 70ea20d388..2862c6684b 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -8,28 +8,28 @@ import (
"net/http"
"slices"
- "code.gitea.io/gitea/models/db"
- org_model "code.gitea.io/gitea/models/organization"
- packages_model "code.gitea.io/gitea/models/packages"
- container_model "code.gitea.io/gitea/models/packages/container"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- alpine_module "code.gitea.io/gitea/modules/packages/alpine"
- arch_model "code.gitea.io/gitea/modules/packages/arch"
- debian_module "code.gitea.io/gitea/modules/packages/debian"
- rpm_module "code.gitea.io/gitea/modules/packages/rpm"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- packages_helper "code.gitea.io/gitea/routers/api/packages/helper"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- packages_service "code.gitea.io/gitea/services/packages"
+ "forgejo.org/models/db"
+ org_model "forgejo.org/models/organization"
+ packages_model "forgejo.org/models/packages"
+ container_model "forgejo.org/models/packages/container"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ alpine_module "forgejo.org/modules/packages/alpine"
+ arch_model "forgejo.org/modules/packages/arch"
+ debian_module "forgejo.org/modules/packages/debian"
+ rpm_module "forgejo.org/modules/packages/rpm"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ packages_helper "forgejo.org/routers/api/packages/helper"
+ shared_user "forgejo.org/routers/web/shared/user"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ packages_service "forgejo.org/services/packages"
)
const (
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index de1c6850aa..02cea05a47 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -1,5 +1,6 @@
// 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
@@ -7,27 +8,29 @@ package user
import (
"errors"
"fmt"
+ gotemplate "html/template"
+ "io"
"net/http"
"path"
"strings"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/routers/web/feed"
- "code.gitea.io/gitea/routers/web/org"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
- user_service "code.gitea.io/gitea/services/user"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/routers/web/feed"
+ "forgejo.org/routers/web/org"
+ shared_user "forgejo.org/routers/web/shared/user"
+ "forgejo.org/services/context"
+ user_service "forgejo.org/services/user"
)
const (
@@ -69,17 +72,6 @@ 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()
@@ -170,11 +162,32 @@ 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{
@@ -253,26 +266,38 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
total = int(count)
case "overview":
- if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
- log.Error("failed to GetBlobContent: %v", err)
+ if rc, _, err := profileReadme.NewTruncatedReader(setting.UI.MaxDisplayFileSize); err != nil {
+ log.Error("failed to NewTruncatedReader: %v", err)
} else {
- 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)
+ 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
+ }
} else {
- ctx.Data["ProfileReadme"] = profileContent
+ 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
}
}
default: // default to "repositories"
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index be5eee90a9..411a356d9b 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -6,12 +6,12 @@ package user
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
+ "forgejo.org/services/convert"
)
// SearchCandidates searches candidate users for dropdown list
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 6f40e39c8d..1dfcc90e35 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -9,23 +9,23 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/auth/password"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/auth/source/db"
- "code.gitea.io/gitea/services/auth/source/smtp"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/mailer"
- "code.gitea.io/gitea/services/user"
+ "forgejo.org/models"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/auth/password"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/validation"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/auth"
+ "forgejo.org/services/auth/source/db"
+ "forgejo.org/services/auth/source/smtp"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/mailer"
+ "forgejo.org/services/user"
)
const (
@@ -57,7 +57,7 @@ func AccountPost(ctx *context.Context) {
return
}
- if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(form.OldPassword) {
+ if ctx.Doer.IsPasswordSet() && !ctx.Doer.ValidatePassword(ctx, 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.IsErrEmailCharIsNotSupported(err) || validation.IsErrEmailInvalid(err) {
+ } else if 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 9fdc5e4d53..3f7e1c13bc 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -7,11 +7,11 @@ import (
"net/http"
"testing"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/contexttest"
- "code.gitea.io/gitea/services/forms"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/contexttest"
+ "forgejo.org/services/forms"
"github.com/stretchr/testify/assert"
)
@@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) {
AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.Status())
})
}
}
diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go
index 171c1933d4..59ff31162b 100644
--- a/routers/web/user/setting/adopt.go
+++ b/routers/web/user/setting/adopt.go
@@ -6,22 +6,18 @@ package setting
import (
"path/filepath"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/context"
- repo_service "code.gitea.io/gitea/services/repository"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/context"
+ repo_service "forgejo.org/services/repository"
)
// 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 4dfd859a44..e73239b79b 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -7,14 +7,14 @@ package setting
import (
"net/http"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
const (
@@ -49,6 +49,9 @@ 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/blocked_users.go b/routers/web/user/setting/blocked_users.go
index 3f35b2eadf..1448dc9a3c 100644
--- a/routers/web/user/setting/blocked_users.go
+++ b/routers/web/user/setting/blocked_users.go
@@ -6,11 +6,11 @@ package setting
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 9462be71c2..935efd7ba7 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -5,18 +5,18 @@
package setting
import (
- "fmt"
+ "errors"
"net/http"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ asymkey_service "forgejo.org/services/asymkey"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
const (
@@ -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", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", errors.New("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", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", errors.New("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", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", errors.New("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", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", errors.New("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", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound("Not Found", errors.New("ssh keys setting is not allowed to be visited"))
return
}
diff --git a/routers/web/user/setting/main_test.go b/routers/web/user/setting/main_test.go
index e398208d0d..38ac2842dd 100644
--- a/routers/web/user/setting/main_test.go
+++ b/routers/web/user/setting/main_test.go
@@ -6,7 +6,7 @@ package setting
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/routers/web/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go
index 1f485e06c8..64b252e97f 100644
--- a/routers/web/user/setting/oauth2.go
+++ b/routers/web/user/setting/oauth2.go
@@ -4,9 +4,9 @@
package setting
import (
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go
index 2132d127b8..7449e45216 100644
--- a/routers/web/user/setting/oauth2_common.go
+++ b/routers/web/user/setting/oauth2_common.go
@@ -7,13 +7,13 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ "forgejo.org/models/auth"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ shared_user "forgejo.org/routers/web/shared/user"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
type OAuth2CommonHandlers struct {
diff --git a/routers/web/user/setting/packages.go b/routers/web/user/setting/packages.go
index 4132659495..ba739a03fc 100644
--- a/routers/web/user/setting/packages.go
+++ b/routers/web/user/setting/packages.go
@@ -7,13 +7,13 @@ import (
"net/http"
"strings"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- chef_module "code.gitea.io/gitea/modules/packages/chef"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- shared "code.gitea.io/gitea/routers/web/shared/packages"
- "code.gitea.io/gitea/services/context"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ chef_module "forgejo.org/modules/packages/chef"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ shared "forgejo.org/routers/web/shared/packages"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 271621872f..e0ce88b582 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -15,23 +15,23 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/avatars"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/typesniffer"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- user_service "code.gitea.io/gitea/services/user"
+ "forgejo.org/models/avatars"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
+ "forgejo.org/modules/typesniffer"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ user_service "forgejo.org/services/user"
)
const (
@@ -51,6 +51,9 @@ 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)
}
@@ -63,6 +66,9 @@ 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)
@@ -145,8 +151,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/runner.go b/routers/web/user/setting/runner.go
index 2bb10cceb9..5c8bba82a1 100644
--- a/routers/web/user/setting/runner.go
+++ b/routers/web/user/setting/runner.go
@@ -4,8 +4,8 @@
package setting
import (
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
func RedirectToDefaultSetting(ctx *context.Context) {
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index 37ccb5e5c4..d23917f8b2 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -12,13 +12,13 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/mailer"
+ "forgejo.org/models/auth"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/mailer"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
@@ -40,11 +40,7 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
return
}
- token, err := t.GenerateScratchToken()
- if err != nil {
- ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
- return
- }
+ token := t.GenerateScratchToken()
if err = auth.UpdateTwoFactor(ctx, t); err != nil {
ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
@@ -60,6 +56,21 @@ 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) {
@@ -86,9 +97,6 @@ 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 {
@@ -176,7 +184,6 @@ 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
@@ -192,6 +199,12 @@ 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
@@ -217,14 +230,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
return
}
- t = &auth.TwoFactor{
+ twoFactor := &auth.TwoFactor{
UID: ctx.Doer.ID,
}
- token, err := t.GenerateScratchToken()
- if err != nil {
- ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
- return
- }
+ token := twoFactor.GenerateScratchToken()
// 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
@@ -246,7 +255,7 @@ func EnrollTwoFactorPost(ctx *context.Context) {
return
}
- if err = auth.NewTwoFactor(ctx, t, secret); err != nil {
+ if err := auth.NewTwoFactor(ctx, twoFactor, 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)
@@ -256,3 +265,41 @@ func EnrollTwoFactorPost(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/openid.go b/routers/web/user/setting/security/openid.go
index 8f788e1735..14660e1646 100644
--- a/routers/web/user/setting/security/openid.go
+++ b/routers/web/user/setting/security/openid.go
@@ -6,13 +6,13 @@ package security
import (
"net/http"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/auth/openid"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/auth/openid"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
)
// OpenIDPost response for change user's openid
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index 8d6859ab87..8b801cfebd 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -8,14 +8,14 @@ import (
"net/http"
"sort"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/auth/source/oauth2"
- "code.gitea.io/gitea/services/context"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/auth/source/oauth2"
+ "forgejo.org/services/context"
)
const (
@@ -55,7 +55,7 @@ func DeleteAccountLink(ctx *context.Context) {
}
func loadSecurityData(ctx *context.Context) {
- enrolled, err := auth_model.HasTwoFactorByUID(ctx, ctx.Doer.ID)
+ enrolled, err := auth_model.HasTOTPByUID(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("SettingsTwoFactor", err)
return
diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go
index bfbc06c701..a909d479c9 100644
--- a/routers/web/user/setting/security/webauthn.go
+++ b/routers/web/user/setting/security/webauthn.go
@@ -9,14 +9,14 @@ import (
"strconv"
"time"
- "code.gitea.io/gitea/models/auth"
- wa "code.gitea.io/gitea/modules/auth/webauthn"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/mailer"
+ "forgejo.org/models/auth"
+ wa "forgejo.org/modules/auth/webauthn"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/mailer"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
diff --git a/routers/web/user/setting/storage_overview.go b/routers/web/user/setting/storage_overview.go
index 8a0c773077..4586600572 100644
--- a/routers/web/user/setting/storage_overview.go
+++ b/routers/web/user/setting/storage_overview.go
@@ -4,9 +4,9 @@
package setting
import (
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/routers/web/shared"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/base"
+ "forgejo.org/routers/web/shared"
+ "forgejo.org/services/context"
)
const (
diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go
index 3cc67d9def..bc07accad4 100644
--- a/routers/web/user/setting/webhooks.go
+++ b/routers/web/user/setting/webhooks.go
@@ -6,12 +6,12 @@ package setting
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
- webhook_service "code.gitea.io/gitea/services/webhook"
+ "forgejo.org/models/db"
+ "forgejo.org/models/webhook"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
+ webhook_service "forgejo.org/services/webhook"
)
const (
diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go
index 38f74ea455..210b32d205 100644
--- a/routers/web/user/stop_watch.go
+++ b/routers/web/user/stop_watch.go
@@ -6,10 +6,10 @@ package user
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/services/context"
+ "forgejo.org/services/convert"
)
// GetStopwatches get all stopwatches
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index 8476767e9e..4059139552 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -7,9 +7,9 @@ import (
"net/http"
"strconv"
- admin_model "code.gitea.io/gitea/models/admin"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/services/context"
+ admin_model "forgejo.org/models/admin"
+ "forgejo.org/modules/json"
+ "forgejo.org/services/context"
)
// TaskStatus returns task's status
@@ -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: "migrate.migrating_failed.error",
+ Format: "repo.migrate.migrating_failed.error",
Args: []any{task.Message},
}
}
diff --git a/routers/web/web.go b/routers/web/web.go
index 15264ccc89..20d5376cfe 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1,53 +1,57 @@
// 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"
- "code.gitea.io/gitea/models/perm"
- quota_model "code.gitea.io/gitea/models/quota"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/metrics"
- "code.gitea.io/gitea/modules/public"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/modules/web/routing"
- "code.gitea.io/gitea/routers/common"
- "code.gitea.io/gitea/routers/web/admin"
- "code.gitea.io/gitea/routers/web/auth"
- "code.gitea.io/gitea/routers/web/devtest"
- "code.gitea.io/gitea/routers/web/events"
- "code.gitea.io/gitea/routers/web/explore"
- "code.gitea.io/gitea/routers/web/feed"
- "code.gitea.io/gitea/routers/web/healthcheck"
- "code.gitea.io/gitea/routers/web/misc"
- "code.gitea.io/gitea/routers/web/org"
- org_setting "code.gitea.io/gitea/routers/web/org/setting"
- "code.gitea.io/gitea/routers/web/repo"
- "code.gitea.io/gitea/routers/web/repo/actions"
- "code.gitea.io/gitea/routers/web/repo/badges"
- repo_flags "code.gitea.io/gitea/routers/web/repo/flags"
- repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
- "code.gitea.io/gitea/routers/web/shared/project"
- "code.gitea.io/gitea/routers/web/user"
- user_setting "code.gitea.io/gitea/routers/web/user/setting"
- "code.gitea.io/gitea/routers/web/user/setting/security"
- auth_service "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/lfs"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/perm"
+ quota_model "forgejo.org/models/quota"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/metrics"
+ "forgejo.org/modules/public"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/templates"
+ "forgejo.org/modules/validation"
+ "forgejo.org/modules/web"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/modules/web/routing"
+ "forgejo.org/routers/common"
+ "forgejo.org/routers/web/admin"
+ "forgejo.org/routers/web/auth"
+ "forgejo.org/routers/web/devtest"
+ "forgejo.org/routers/web/events"
+ "forgejo.org/routers/web/explore"
+ "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"
+ "forgejo.org/routers/web/repo/actions"
+ "forgejo.org/routers/web/repo/badges"
+ repo_flags "forgejo.org/routers/web/repo/flags"
+ repo_setting "forgejo.org/routers/web/repo/setting"
+ "forgejo.org/routers/web/shared/project"
+ "forgejo.org/routers/web/user"
+ user_setting "forgejo.org/routers/web/user/setting"
+ "forgejo.org/routers/web/user/setting/security"
+ auth_service "forgejo.org/services/auth"
+ "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/lfs"
- _ "code.gitea.io/gitea/modules/session" // to registers all internal adapters
+ _ "forgejo.org/modules/session" // to registers all internal adapters
"code.forgejo.org/go-chi/captcha"
chi_middleware "github.com/go-chi/chi/v5/middleware"
@@ -115,7 +119,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.Error("Failed to verify user: %v", err)
+ log.Info("Failed to verify user: %v", err)
ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.unauthorized_credentials", "https://codeberg.org/forgejo/forgejo/issues/2809"))
return
}
@@ -167,6 +171,19 @@ 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.
@@ -175,7 +192,8 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
return
}
- if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" {
+ safeMethod := ctx.Req.Method == "GET" || ctx.Req.Method == "HEAD" || ctx.Req.Method == "OPTIONS"
+ if !options.SignOutRequired && !options.DisableCSRF && !safeMethod {
ctx.Csrf.Validate(ctx)
if ctx.Written() {
return
@@ -307,6 +325,20 @@ 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)
@@ -473,6 +505,11 @@ 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)
@@ -557,6 +594,8 @@ 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)
@@ -569,7 +608,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
@@ -774,7 +813,14 @@ func registerRoutes(m *web.Route) {
addSettingsRunnersRoutes()
addSettingsVariablesRoutes()
})
- }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled))
+
+ 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))
// ***** END: Admin *****
m.Group("", func() {
@@ -811,13 +857,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 {
- case ctx.ContextUser.Visibility == structs.VisibleTypePrivate:
+ switch ctx.ContextUser.Visibility {
+ case structs.VisibleTypePrivate:
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
ctx.NotFound("Visit Project", nil)
return
}
- case ctx.ContextUser.Visibility == structs.VisibleTypeLimited:
+ case structs.VisibleTypeLimited:
if ctx.Doer == nil {
ctx.NotFound("Visit Project", nil)
return
@@ -1392,25 +1438,28 @@ func registerRoutes(m *web.Route) {
m.Get("", actions.List)
m.Post("/disable", reqRepoAdmin, actions.DisableWorkflowFile)
m.Post("/enable", reqRepoAdmin, actions.EnableWorkflowFile)
- m.Post("/manual", reqRepoAdmin, actions.ManualRunWorkflow)
+ m.Post("/manual", reqRepoActionsWriter, actions.ManualRunWorkflow)
m.Group("/runs", func() {
m.Get("/latest", actions.ViewLatest)
m.Group("/{run}", func() {
m.Combo("").
- Get(actions.View).
+ Get(actions.RedirectToLatestAttempt).
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
m.Group("/jobs/{job}", func() {
m.Combo("").
- Get(actions.View).
+ Get(actions.RedirectToLatestAttempt).
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}", actions.ArtifactsDownloadView)
+ m.Get("/artifacts/{artifact_name_or_id}", actions.ArtifactsDownloadView)
m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
})
@@ -1454,7 +1503,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.RecentCommitsData)
+ m.Get("/data", repo.CodeFrequencyData)
}, repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypeCode))
}, context.RepoRef(), context.RequireRepoReaderOr(unit.TypeCode, unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
@@ -1503,7 +1552,10 @@ 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.Get("/{sha:[a-f0-9]{4,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
+ 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.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)
@@ -1591,6 +1643,8 @@ 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)
@@ -1661,6 +1715,7 @@ 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 1f3de70db0..5f67e436bf 100644
--- a/routers/web/webfinger.go
+++ b/routers/web/webfinger.go
@@ -9,10 +9,10 @@ import (
"net/url"
"strings"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
// https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-webfinger-14#section-4.4
@@ -58,7 +58,33 @@ 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 {
@@ -153,7 +179,7 @@ func WebfingerQuery(ctx *context.Context) {
},
{
Rel: "http://openid.net/specs/connect/1.0/issuer",
- Href: appURL.String(),
+ Href: strings.TrimSuffix(appURL.String(), "/"),
},
}
diff --git a/services/actions/TestServiceActions_startTask/action_schedule.yml b/services/actions/TestServiceActions_startTask/action_schedule.yml
new file mode 100644
index 0000000000..d0e7234475
--- /dev/null
+++ b/services/actions/TestServiceActions_startTask/action_schedule.yml
@@ -0,0 +1,41 @@
+# 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
new file mode 100644
index 0000000000..7bcc78f010
--- /dev/null
+++ b/services/actions/TestServiceActions_startTask/action_schedule_spec.yml
@@ -0,0 +1,15 @@
+# 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 1ef21f6e0e..98b618aeba 100644
--- a/services/actions/auth.go
+++ b/services/actions/auth.go
@@ -4,14 +4,15 @@
package actions
import (
+ "errors"
"fmt"
"net/http"
"strings"
"time"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
"github.com/golang-jwt/jwt/v5"
)
@@ -80,7 +81,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, fmt.Errorf("split token failed")
+ return 0, errors.New("split token failed")
}
return TokenToTaskID(parts[1])
@@ -100,7 +101,7 @@ func TokenToTaskID(token string) (int64, error) {
c, ok := parsedToken.Claims.(*actionsClaims)
if !parsedToken.Valid || !ok {
- return 0, fmt.Errorf("invalid token claim")
+ return 0, errors.New("invalid token claim")
}
return c.TaskID, nil
diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go
index 1400e61f47..d9f0437e1b 100644
--- a/services/actions/auth_test.go
+++ b/services/actions/auth_test.go
@@ -7,8 +7,8 @@ import (
"net/http"
"testing"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/setting"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
@@ -19,7 +19,7 @@ func TestCreateAuthorizationToken(t *testing.T) {
var taskID int64 = 23
token, err := CreateAuthorizationToken(taskID, 1, 2)
require.NoError(t, err)
- assert.NotEqual(t, "", token)
+ assert.NotEmpty(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.NotEqual(t, "", token)
+ assert.NotEmpty(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 34fa2688e7..918be0f185 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -10,12 +10,12 @@ import (
"os"
"time"
- actions_model "code.gitea.io/gitea/models/actions"
- actions_module "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/timeutil"
+ actions_model "forgejo.org/models/actions"
+ actions_module "forgejo.org/modules/actions"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/timeutil"
)
// Cleanup removes expired actions logs, data and artifacts
@@ -126,3 +126,9 @@ 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 65fae840c1..4a847ced23 100644
--- a/services/actions/cleanup_test.go
+++ b/services/actions/cleanup_test.go
@@ -6,10 +6,10 @@ package actions
import (
"testing"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/timeutil"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.EqualValues(t, "does-not-exist", task.LogFilename)
+ assert.Equal(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 f146c22372..c36dda55b2 100644
--- a/services/actions/clear_tasks.go
+++ b/services/actions/clear_tasks.go
@@ -8,12 +8,12 @@ import (
"fmt"
"time"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/actions"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
)
// StopZombieTasks stops the task which have running status, but haven't been updated for a long time
@@ -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 := actions_model.StopTask(ctx, task.ID, actions_model.StatusFailure); err != nil {
+ if err := 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 := actions_model.UpdateRunJob(ctx, job, nil, "status", "stopped")
+ _, err := 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 04dffbac88..d45b75ac0b 100644
--- a/services/actions/commit_status.go
+++ b/services/actions/commit_status.go
@@ -5,20 +5,21 @@ package actions
import (
"context"
+ "errors"
"fmt"
"path"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- user_model "code.gitea.io/gitea/models/user"
- actions_module "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ user_model "forgejo.org/models/user"
+ actions_module "forgejo.org/modules/actions"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
+ commitstatus_service "forgejo.org/services/repository/commitstatus"
- "github.com/nektos/act/pkg/jobparser"
+ "code.forgejo.org/forgejo/runner/v11/act/jobparser"
)
// CreateCommitStatus creates a commit status for the given job.
@@ -50,7 +51,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
return fmt.Errorf("GetPushEventPayload: %w", err)
}
if payload.HeadCommit == nil {
- return fmt.Errorf("head commit is missing in event payload")
+ return errors.New("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:
@@ -64,9 +65,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
return fmt.Errorf("GetPullRequestEventPayload: %w", err)
}
if payload.PullRequest == nil {
- return fmt.Errorf("pull request is missing in event payload")
+ return errors.New("pull request is missing in event payload")
} else if payload.PullRequest.Head == nil {
- return fmt.Errorf("head of pull request is missing in event payload")
+ return errors.New("head of pull request is missing in event payload")
}
sha = payload.PullRequest.Head.Sha
case webhook_module.HookEventRelease:
@@ -79,7 +80,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); err == nil && len(wfs) > 0 {
+ if wfs, err := jobparser.Parse(job.WorkflowPayload, false); err == nil && len(wfs) > 0 {
runName = wfs[0].Name
}
ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event)
diff --git a/services/actions/context.go b/services/actions/context.go
index be1c85522b..bf187c56bf 100644
--- a/services/actions/context.go
+++ b/services/actions/context.go
@@ -7,13 +7,13 @@ import (
"context"
"fmt"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- actions_module "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ actions_module "forgejo.org/modules/actions"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/setting"
)
// GenerateGiteaContext generate the gitea context without token and gitea_runtime_token
diff --git a/services/actions/context_test.go b/services/actions/context_test.go
index 4cd8825870..c96094ade8 100644
--- a/services/actions/context_test.go
+++ b/services/actions/context_test.go
@@ -6,8 +6,8 @@ package actions
import (
"testing"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/unittest"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/actions/init.go b/services/actions/init.go
index 0f49cb6297..8f1db64e27 100644
--- a/services/actions/init.go
+++ b/services/actions/init.go
@@ -4,11 +4,11 @@
package actions
import (
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
+ notify_service "forgejo.org/services/notify"
)
func Init() {
diff --git a/services/actions/interface.go b/services/actions/interface.go
index 76bee6f153..54a30061bc 100644
--- a/services/actions/interface.go
+++ b/services/actions/interface.go
@@ -3,7 +3,7 @@
package actions
-import "code.gitea.io/gitea/services/context"
+import "forgejo.org/services/context"
// API for actions of a repository or organization
type API interface {
diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go
index 1f859fcf70..8e58105e47 100644
--- a/services/actions/job_emitter.go
+++ b/services/actions/job_emitter.go
@@ -8,12 +8,12 @@ import (
"errors"
"fmt"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/queue"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/queue"
- "github.com/nektos/act/pkg/jobparser"
+ "code.forgejo.org/forgejo/runner/v11/act/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 := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": actions_model.StatusBlocked}, "status"); err != nil {
+ if n, err := 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); len(wfJobs) == 1 {
+ if wfJobs, _ := jobparser.Parse(r.jobMap[id].WorkflowPayload, false); len(wfJobs) == 1 {
_, wfJob := wfJobs[0].Job()
hasIf = len(wfJob.If.Value) > 0
}
diff --git a/services/actions/job_emitter_test.go b/services/actions/job_emitter_test.go
index 58c2dc3b24..a3e0e95d04 100644
--- a/services/actions/job_emitter_test.go
+++ b/services/actions/job_emitter_test.go
@@ -6,7 +6,7 @@ package actions
import (
"testing"
- actions_model "code.gitea.io/gitea/models/actions"
+ actions_model "forgejo.org/models/actions"
"github.com/stretchr/testify/assert"
)
diff --git a/services/actions/job_parser.go b/services/actions/job_parser.go
new file mode 100644
index 0000000000..8c880977d1
--- /dev/null
+++ b/services/actions/job_parser.go
@@ -0,0 +1,31 @@
+// 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
new file mode 100644
index 0000000000..9c1361d74e
--- /dev/null
+++ b/services/actions/job_parser_test.go
@@ -0,0 +1,212 @@
+// 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/main_test.go b/services/actions/main_test.go
index 49629ecb03..71ec1d3426 100644
--- a/services/actions/main_test.go
+++ b/services/actions/main_test.go
@@ -6,11 +6,11 @@ package actions
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/activities"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/activities"
+ _ "forgejo.org/models/forgefed"
)
func TestMain(m *testing.M) {
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 2dd81158a7..7b291d491b 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -5,21 +5,26 @@ package actions
import (
"context"
+ "errors"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- perm_model "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/convert"
- notify_service "code.gitea.io/gitea/services/notify"
+ actions_model "forgejo.org/models/actions"
+ issues_model "forgejo.org/models/issues"
+ packages_model "forgejo.org/models/packages"
+ perm_model "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "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 {
@@ -222,7 +227,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, nil)
+ apiLabel = convert.ToLabel(label, issue.Repo, issue.Repo.Owner)
}
if issue.IsPull {
@@ -338,7 +343,7 @@ func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, commen
newNotifyInputFromIssue(comment.Issue, event).
WithDoer(doer).
WithPayload(payload).
- WithPullRequest(comment.Issue.PullRequest).
+ WithPullRequestData(comment.Issue.PullRequest).
Notify(ctx)
return
}
@@ -775,3 +780,76 @@ 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 0a1dbb162d..a9978c32cb 100644
--- a/services/actions/notifier_helper.go
+++ b/services/actions/notifier_helper.go
@@ -11,27 +11,27 @@ import (
"slices"
"strings"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- actions_module "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/convert"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ packages_model "forgejo.org/models/packages"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ actions_module "forgejo.org/modules/actions"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/convert"
- "github.com/nektos/act/pkg/jobparser"
- "github.com/nektos/act/pkg/model"
+ "code.forgejo.org/forgejo/runner/v11/act/jobparser"
+ "code.forgejo.org/forgejo/runner/v11/act/model"
)
type methodCtx struct{}
@@ -102,6 +102,12 @@ 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 == "" {
@@ -139,7 +145,7 @@ func notify(ctx context.Context, input *notifyInput) error {
return nil
}
if unit_model.TypeActions.UnitGlobalDisabled() {
- if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo, true); err != nil {
+ if err := CleanRepoScheduleTasks(ctx, input.Repo, true); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
return nil
@@ -219,7 +225,7 @@ func notify(ctx context.Context, input *notifyInput) error {
}
}
- if input.PullRequest != nil {
+ if input.PullRequest != nil && !actions_module.IsDefaultBranchWorkflow(input.Event) {
// detect pull_request_target workflows
baseRef := git.BranchPrefix + input.PullRequest.BaseBranch
baseCommit, err := gitRepo.GetCommit(baseRef)
@@ -315,7 +321,7 @@ func handleWorkflows(
}
isForkPullRequest := false
- if pr := input.PullRequest; pr != nil {
+ if pr := input.PullRequest; pr != nil && !actions_module.IsDefaultBranchWorkflow(input.Event) {
switch pr.Flow {
case issues_model.PullRequestFlowGithub:
isForkPullRequest = pr.IsFromFork()
@@ -345,6 +351,14 @@ 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)
@@ -364,16 +378,19 @@ func handleWorkflows(
continue
}
- jobs, err := jobparser.Parse(dwf.Content, jobparser.WithVars(vars))
+ jobs, err := jobParser(dwf.Content, jobparser.WithVars(vars))
if err != nil {
- log.Error("jobparser.Parse: %v", err)
- continue
+ run.Status = actions_model.StatusFailure
+ log.Info("jobparser.Parse: invalid workflow, setting job status to failed: %v", err)
+ jobs = []*jobparser.SingleWorkflow{{
+ Name: dwf.EntryName,
+ }}
}
// 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 := actions_model.CancelPreviousJobs(
+ if err := CancelPreviousJobs(
ctx,
run.RepoID,
run.Ref,
@@ -504,7 +521,7 @@ func handleSchedules(
log.Error("CountSchedules: %v", err)
return err
} else if count > 0 {
- if err := actions_model.CleanRepoScheduleTasks(ctx, input.Repo, false); err != nil {
+ if err := CleanRepoScheduleTasks(ctx, input.Repo, false); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
}
@@ -526,7 +543,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))
+ workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content), false)
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 0fa40c0168..525103927b 100644
--- a/services/actions/notifier_helper_test.go
+++ b/services/actions/notifier_helper_test.go
@@ -6,11 +6,18 @@ package actions
import (
"testing"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ 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"
)
@@ -49,3 +56,91 @@ 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/rerun.go b/services/actions/rerun.go
index 60f6650905..f6dd4af5c7 100644
--- a/services/actions/rerun.go
+++ b/services/actions/rerun.go
@@ -4,8 +4,8 @@
package actions
import (
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/modules/container"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/modules/container"
)
// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
diff --git a/services/actions/rerun_test.go b/services/actions/rerun_test.go
index a98de7b788..4b822e8da1 100644
--- a/services/actions/rerun_test.go
+++ b/services/actions/rerun_test.go
@@ -6,7 +6,7 @@ package actions
import (
"testing"
- actions_model "code.gitea.io/gitea/models/actions"
+ actions_model "forgejo.org/models/actions"
"github.com/stretchr/testify/assert"
)
diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go
index 18f3324fd2..07ff7b3187 100644
--- a/services/actions/schedule_tasks.go
+++ b/services/actions/schedule_tasks.go
@@ -4,19 +4,24 @@
package actions
import (
+ "bytes"
"context"
+ "errors"
"fmt"
"time"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/timeutil"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/timeutil"
+ webhook_module "forgejo.org/modules/webhook"
- "github.com/nektos/act/pkg/jobparser"
+ "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"
)
// StartScheduleTasks start the task
@@ -55,7 +60,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 := actions_model.CancelPreviousJobs(
+ if err := CancelPreviousJobs(
ctx,
row.RepoID,
row.Schedule.Ref,
@@ -79,20 +84,33 @@ func startTasks(ctx context.Context) error {
}
return fmt.Errorf("GetUnit: %w", err)
}
- if cfg.ActionsConfig().IsWorkflowDisabled(row.Schedule.WorkflowID) {
+ actionConfig := cfg.ActionsConfig()
+ if actionConfig.IsWorkflowDisabled(row.Schedule.WorkflowID) {
continue
}
- if err := CreateScheduleTask(ctx, row.Schedule); err != nil {
- log.Error("CreateScheduleTask: %v", err)
- return err
+ 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
}
- // Parse the spec
- schedule, err := row.Parse()
+ schedule, err := createAndSchedule(row)
if err != nil {
- log.Error("Parse: %v", err)
- return err
+ 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
}
// Update the spec's next run time and previous run time
@@ -138,8 +156,18 @@ 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.Parse(cron.Content, jobparser.WithVars(vars))
+ workflows, err := jobParser(cron.Content, jobparser.WithVars(vars))
if err != nil {
return err
}
@@ -152,3 +180,93 @@ 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
new file mode 100644
index 0000000000..31ed5ec813
--- /dev/null
+++ b/services/actions/schedule_tasks_test.go
@@ -0,0 +1,175 @@
+// 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 bc54ade347..bb319c7d05 100644
--- a/services/actions/task.go
+++ b/services/actions/task.go
@@ -5,14 +5,18 @@ package actions
import (
"context"
+ "errors"
"fmt"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- secret_model "code.gitea.io/gitea/models/secret"
+ 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) {
@@ -105,3 +109,144 @@ 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 a5703898ab..a9f42105a3 100644
--- a/services/actions/variables.go
+++ b/services/actions/variables.go
@@ -8,10 +8,10 @@ import (
"regexp"
"strings"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
- secret_service "code.gitea.io/gitea/services/secrets"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/util"
+ secret_service "forgejo.org/services/secrets"
)
func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
@@ -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
new file mode 100644
index 0000000000..f69bc674e1
--- /dev/null
+++ b/services/actions/variables_test.go
@@ -0,0 +1,17 @@
+// 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 e3e342264d..27d05c9043 100644
--- a/services/actions/workflows.go
+++ b/services/actions/workflows.go
@@ -10,22 +10,22 @@ import (
"fmt"
"strconv"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/perm"
- "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/convert"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/perm"
+ "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/user"
+ "forgejo.org/modules/actions"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/webhook"
+ "forgejo.org/services/convert"
- "github.com/nektos/act/pkg/jobparser"
- act_model "github.com/nektos/act/pkg/model"
+ "code.forgejo.org/forgejo/runner/v11/act/jobparser"
+ act_model "code.forgejo.org/forgejo/runner/v11/act/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))
+ wf, err := act_model.ReadWorkflow(bytes.NewReader(content), false)
if err != nil {
return nil, nil, err
}
@@ -111,6 +111,11 @@ 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,
@@ -125,6 +130,7 @@ 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)
@@ -132,7 +138,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
return nil, nil, err
}
- jobs, err := jobparser.Parse(content, jobparser.WithVars(vars))
+ jobs, err := jobParser(content, jobparser.WithVars(vars))
if err != nil {
return nil, nil, err
}
diff --git a/services/agit/agit.go b/services/agit/agit.go
index a18f9ef728..8ef641629a 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -9,15 +9,15 @@ import (
"os"
"strings"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/git/pushoptions"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/private"
- notify_service "code.gitea.io/gitea/services/notify"
- pull_service "code.gitea.io/gitea/services/pull"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/git/pushoptions"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/private"
+ notify_service "forgejo.org/services/notify"
+ pull_service "forgejo.org/services/pull"
)
// ProcReceive handle proc receive work
@@ -221,7 +221,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
}
// Validate pull request.
- pull_service.ValidatePullRequest(ctx, pr, oldCommitID, opts.NewCommitIDs[i], pusher)
+ pull_service.ValidatePullRequest(ctx, pr, opts.NewCommitIDs[i], oldCommitID, pusher)
// TODO: call `InvalidateCodeComments`
diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go
index e127cbfc6e..4a2cb53eec 100644
--- a/services/asymkey/deploy_key.go
+++ b/services/asymkey/deploy_key.go
@@ -6,10 +6,10 @@ package asymkey
import (
"context"
- "code.gitea.io/gitea/models"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
)
// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
diff --git a/services/asymkey/main_test.go b/services/asymkey/main_test.go
index 060cc78cec..8ba76668b1 100644
--- a/services/asymkey/main_test.go
+++ b/services/asymkey/main_test.go
@@ -6,11 +6,11 @@ package asymkey
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/activities"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/activities"
+ _ "forgejo.org/models/forgefed"
)
func TestMain(m *testing.M) {
diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go
index 8fb569939c..527f6edd92 100644
--- a/services/asymkey/sign.go
+++ b/services/asymkey/sign.go
@@ -8,18 +8,17 @@ import (
"fmt"
"strings"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/setting"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/auth"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/setting"
)
type signingMode string
@@ -90,6 +89,13 @@ 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})
@@ -145,22 +151,19 @@ Loop:
case always:
break Loop
case pubkey:
- keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
- OwnerID: u.ID,
- IncludeSubKeys: true,
- })
+ hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
if err != nil {
return false, "", nil, err
}
- if len(keys) == 0 {
+ if !hasPubKey {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
- if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
+ if err != nil {
return false, "", nil, err
}
- if twofaModel == nil {
+ if !hasTwoFactor {
return false, "", nil, &ErrWontSign{twofa}
}
}
@@ -185,22 +188,19 @@ Loop:
case always:
break Loop
case pubkey:
- keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
- OwnerID: u.ID,
- IncludeSubKeys: true,
- })
+ hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
if err != nil {
return false, "", nil, err
}
- if len(keys) == 0 {
+ if !hasPubKey {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
- if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
+ if err != nil {
return false, "", nil, err
}
- if twofaModel == nil {
+ if !hasTwoFactor {
return false, "", nil, &ErrWontSign{twofa}
}
case parentSigned:
@@ -241,22 +241,19 @@ Loop:
case always:
break Loop
case pubkey:
- keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
- OwnerID: u.ID,
- IncludeSubKeys: true,
- })
+ hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
if err != nil {
return false, "", nil, err
}
- if len(keys) == 0 {
+ if !hasPubKey {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
- if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
+ if err != nil {
return false, "", nil, err
}
- if twofaModel == nil {
+ if !hasTwoFactor {
return false, "", nil, &ErrWontSign{twofa}
}
case parentSigned:
@@ -306,22 +303,19 @@ Loop:
case always:
break Loop
case pubkey:
- keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
- OwnerID: u.ID,
- IncludeSubKeys: true,
- })
+ hasPubKey, err := asymkey_model.HasAsymKeyByUID(ctx, u.ID)
if err != nil {
return false, "", nil, err
}
- if len(keys) == 0 {
+ if !hasPubKey {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
- twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
- if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
+ hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, u.ID)
+ if err != nil {
return false, "", nil, err
}
- if twofaModel == nil {
+ if !hasTwoFactor {
return false, "", nil, &ErrWontSign{twofa}
}
case approved:
diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go
index 83d7edafa3..f20445891d 100644
--- a/services/asymkey/ssh_key.go
+++ b/services/asymkey/ssh_key.go
@@ -6,9 +6,9 @@ package asymkey
import (
"context"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
)
// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
diff --git a/services/asymkey/ssh_key_test.go b/services/asymkey/ssh_key_test.go
index d667a02557..24b28d295e 100644
--- a/services/asymkey/ssh_key_test.go
+++ b/services/asymkey/ssh_key_test.go
@@ -6,11 +6,11 @@ package asymkey
import (
"testing"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/attachment/attachment.go b/services/attachment/attachment.go
index c911945e5d..b6f763842b 100644
--- a/services/attachment/attachment.go
+++ b/services/attachment/attachment.go
@@ -9,12 +9,12 @@ import (
"fmt"
"io"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/services/context/upload"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/validation"
+ "forgejo.org/services/context/upload"
"github.com/google/uuid"
)
@@ -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.IsValidExternalURL(attach.ExternalURL) {
+ if !validation.IsValidReleaseAssetURL(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 c24b3f8006..ef002bf16c 100644
--- a/services/attachment/attachment_test.go
+++ b/services/attachment/attachment_test.go
@@ -8,13 +8,13 @@ import (
"path/filepath"
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -43,6 +43,6 @@ func TestUploadAttachment(t *testing.T) {
attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attach.UUID)
require.NoError(t, err)
- assert.EqualValues(t, user.ID, attachment.UploaderID)
+ assert.Equal(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 c10872313f..85121b2d7f 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -10,15 +10,15 @@ import (
"regexp"
"strings"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/auth/webauthn"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/session"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web/middleware"
- gitea_context "code.gitea.io/gitea/services/context"
- user_service "code.gitea.io/gitea/services/user"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/auth/webauthn"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/session"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web/middleware"
+ gitea_context "forgejo.org/services/context"
+ user_service "forgejo.org/services/user"
)
// Init should be called exactly once when the application starts to allow plugins
@@ -77,6 +77,7 @@ 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/auth_test.go b/services/auth/auth_test.go
index 3adaa28664..a6c6c74022 100644
--- a/services/auth/auth_test.go
+++ b/services/auth/auth_test.go
@@ -8,7 +8,7 @@ import (
"net/http"
"testing"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/setting"
)
func Test_isGitRawOrLFSPath(t *testing.T) {
diff --git a/services/auth/basic.go b/services/auth/basic.go
index d489164954..4ffe712744 100644
--- a/services/auth/basic.go
+++ b/services/auth/basic.go
@@ -9,15 +9,15 @@ import (
"net/http"
"strings"
- actions_model "code.gitea.io/gitea/models/actions"
- auth_model "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web/middleware"
+ actions_model "forgejo.org/models/actions"
+ auth_model "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/web/middleware"
)
// Ensure the struct implements the interface.
@@ -151,6 +151,7 @@ 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/group.go b/services/auth/group.go
index aecf43cb24..b713301b50 100644
--- a/services/auth/group.go
+++ b/services/auth/group.go
@@ -7,7 +7,7 @@ import (
"net/http"
"strings"
- user_model "code.gitea.io/gitea/models/user"
+ user_model "forgejo.org/models/user"
)
// Ensure the struct implements the interface.
diff --git a/services/auth/httpsign.go b/services/auth/httpsign.go
index 83a36bef23..e776ccbbed 100644
--- a/services/auth/httpsign.go
+++ b/services/auth/httpsign.go
@@ -11,11 +11,11 @@ import (
"net/http"
"strings"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
"github.com/42wim/httpsig"
"golang.org/x/crypto/ssh"
@@ -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, fmt.Errorf("no certificate found")
+ return nil, errors.New("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, fmt.Errorf("CA check failed")
+ return nil, errors.New("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, fmt.Errorf("no valid principal found")
+ return nil, errors.New("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/interface.go b/services/auth/interface.go
index ece28af12d..12b04a7abf 100644
--- a/services/auth/interface.go
+++ b/services/auth/interface.go
@@ -7,9 +7,9 @@ import (
"context"
"net/http"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/session"
- "code.gitea.io/gitea/modules/web/middleware"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/session"
+ "forgejo.org/modules/web/middleware"
)
// DataStore represents a data store
diff --git a/services/auth/main_test.go b/services/auth/main_test.go
index b81c39a1f2..0e6315b06e 100644
--- a/services/auth/main_test.go
+++ b/services/auth/main_test.go
@@ -6,7 +6,7 @@ package auth
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go
index b983e57ecd..fa13c20a7f 100644
--- a/services/auth/oauth2.go
+++ b/services/auth/oauth2.go
@@ -11,15 +11,16 @@ import (
"strings"
"time"
- actions_model "code.gitea.io/gitea/models/actions"
- auth_model "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/actions"
- "code.gitea.io/gitea/services/auth/source/oauth2"
+ actions_model "forgejo.org/models/actions"
+ auth_model "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ "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"
)
// Ensure the struct implements the interface.
@@ -137,7 +138,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 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
+ if len(auths) == 2 && (util.ASCIIEqualFold(auths[0], "token") || util.ASCIIEqualFold(auths[0], "bearer")) {
return auths[1], true
}
}
diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go
index 90e2fe4517..3fce7df50b 100644
--- a/services/auth/oauth2_test.go
+++ b/services/auth/oauth2_test.go
@@ -4,12 +4,13 @@
package auth
import (
+ "net/http"
"testing"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/actions"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -52,3 +53,30 @@ 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.go b/services/auth/reverseproxy.go
index 8a5a5dc992..eb9ceb8cf2 100644
--- a/services/auth/reverseproxy.go
+++ b/services/auth/reverseproxy.go
@@ -8,11 +8,11 @@ import (
"net/http"
"strings"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web/middleware"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web/middleware"
gouuid "github.com/google/uuid"
)
diff --git a/services/auth/reverseproxy_test.go b/services/auth/reverseproxy_test.go
index 7f1b2a7782..cdcd845148 100644
--- a/services/auth/reverseproxy_test.go
+++ b/services/auth/reverseproxy_test.go
@@ -7,11 +7,11 @@ import (
"net/http"
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/require"
)
@@ -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.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.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.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.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.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.False(t, user.IsAdmin)
})
}
diff --git a/services/auth/session.go b/services/auth/session.go
index 35d97e42da..a15c24c940 100644
--- a/services/auth/session.go
+++ b/services/auth/session.go
@@ -6,8 +6,8 @@ package auth
import (
"net/http"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
)
// Ensure the struct implements the interface.
diff --git a/services/auth/signin.go b/services/auth/signin.go
index 7c69da8f94..495b3d387e 100644
--- a/services/auth/signin.go
+++ b/services/auth/signin.go
@@ -7,17 +7,17 @@ import (
"context"
"strings"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/services/auth/source/oauth2"
- "code.gitea.io/gitea/services/auth/source/smtp"
+ "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/services/auth/source/oauth2"
+ "forgejo.org/services/auth/source/smtp"
- _ "code.gitea.io/gitea/services/auth/source/db" // register the sources (and below)
- _ "code.gitea.io/gitea/services/auth/source/ldap" // register the ldap source
- _ "code.gitea.io/gitea/services/auth/source/pam" // register the pam source
+ _ "forgejo.org/services/auth/source/db" // register the sources (and below)
+ _ "forgejo.org/services/auth/source/ldap" // register the ldap source
+ _ "forgejo.org/services/auth/source/pam" // register the pam source
)
// UserSignIn validates user name and password.
diff --git a/services/auth/source.go b/services/auth/source.go
index 69b71a6dea..b13554efde 100644
--- a/services/auth/source.go
+++ b/services/auth/source.go
@@ -6,9 +6,9 @@ package auth
import (
"context"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
)
// DeleteSource deletes a AuthSource record in DB.
diff --git a/services/auth/source/db/assert_interface_test.go b/services/auth/source/db/assert_interface_test.go
index 62387c78f0..1422e9693c 100644
--- a/services/auth/source/db/assert_interface_test.go
+++ b/services/auth/source/db/assert_interface_test.go
@@ -4,9 +4,9 @@
package db_test
import (
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/auth/source/db"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/services/auth"
+ "forgejo.org/services/auth/source/db"
)
// This test file exists to assert that our Source exposes the interfaces that we expect
diff --git a/services/auth/source/db/authenticate.go b/services/auth/source/db/authenticate.go
index 8160141863..b1d8eae6ae 100644
--- a/services/auth/source/db/authenticate.go
+++ b/services/auth/source/db/authenticate.go
@@ -7,9 +7,9 @@ import (
"context"
"fmt"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
)
// ErrUserPasswordNotSet represents a "ErrUserPasswordNotSet" kind of error.
@@ -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(password) {
+ } else if !user.ValidatePassword(ctx, password) {
return nil, ErrUserPasswordInvalid{UID: user.ID, Name: user.Name}
}
diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go
index bb2270cbd6..d158718bb2 100644
--- a/services/auth/source/db/source.go
+++ b/services/auth/source/db/source.go
@@ -6,8 +6,8 @@ package db
import (
"context"
- "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
)
// Source is a password authentication service
diff --git a/services/auth/source/ldap/assert_interface_test.go b/services/auth/source/ldap/assert_interface_test.go
index 33347687dc..859143a3f8 100644
--- a/services/auth/source/ldap/assert_interface_test.go
+++ b/services/auth/source/ldap/assert_interface_test.go
@@ -4,9 +4,9 @@
package ldap_test
import (
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/auth/source/ldap"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/services/auth"
+ "forgejo.org/services/auth/source/ldap"
)
// This test file exists to assert that our Source exposes the interfaces that we expect
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index ba407b351a..a094c1410c 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -6,10 +6,10 @@ package ldap
import (
"strings"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/secret"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/auth"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/secret"
+ "forgejo.org/modules/setting"
)
// .____ ________ _____ __________
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 68ecd16342..a2ff10cd07 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -8,13 +8,13 @@ import (
"fmt"
"strings"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- auth_module "code.gitea.io/gitea/modules/auth"
- "code.gitea.io/gitea/modules/optional"
- source_service "code.gitea.io/gitea/services/auth/source"
- user_service "code.gitea.io/gitea/services/user"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ auth_module "forgejo.org/modules/auth"
+ "forgejo.org/modules/optional"
+ source_service "forgejo.org/services/auth/source"
+ user_service "forgejo.org/services/user"
)
// Authenticate queries if login/password is valid against the LDAP directory pool,
diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index 2a61386ae1..da7e225428 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -11,8 +11,8 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
"github.com/go-ldap/ldap/v3"
)
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 1f70edaa82..cb6172ed1d 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -8,16 +8,16 @@ import (
"fmt"
"strings"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- user_model "code.gitea.io/gitea/models/user"
- auth_module "code.gitea.io/gitea/modules/auth"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- source_service "code.gitea.io/gitea/services/auth/source"
- user_service "code.gitea.io/gitea/services/user"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ user_model "forgejo.org/models/user"
+ auth_module "forgejo.org/modules/auth"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ source_service "forgejo.org/services/auth/source"
+ user_service "forgejo.org/services/user"
)
// Sync causes this ldap source to synchronize its users with the db
diff --git a/services/auth/source/oauth2/assert_interface_test.go b/services/auth/source/oauth2/assert_interface_test.go
index 56fe0e4aa8..12fce257cf 100644
--- a/services/auth/source/oauth2/assert_interface_test.go
+++ b/services/auth/source/oauth2/assert_interface_test.go
@@ -4,9 +4,9 @@
package oauth2_test
import (
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/auth/source/oauth2"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/services/auth"
+ "forgejo.org/services/auth/source/oauth2"
)
// This test file exists to assert that our Source exposes the interfaces that we expect
diff --git a/services/auth/source/oauth2/init.go b/services/auth/source/oauth2/init.go
index 5c25681548..6c78a14da4 100644
--- a/services/auth/source/oauth2/init.go
+++ b/services/auth/source/oauth2/init.go
@@ -9,11 +9,11 @@ import (
"net/http"
"sync"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
"github.com/google/uuid"
"github.com/gorilla/sessions"
diff --git a/services/auth/source/oauth2/jwtsigningkey.go b/services/auth/source/oauth2/jwtsigningkey.go
index 92adfc4d84..550945a812 100644
--- a/services/auth/source/oauth2/jwtsigningkey.go
+++ b/services/auth/source/oauth2/jwtsigningkey.go
@@ -18,9 +18,9 @@ import (
"path/filepath"
"strings"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
"github.com/golang-jwt/jwt/v5"
)
diff --git a/services/auth/source/oauth2/jwtsigningkey_test.go b/services/auth/source/oauth2/jwtsigningkey_test.go
index 4db538b0e8..9b07b022df 100644
--- a/services/auth/source/oauth2/jwtsigningkey_test.go
+++ b/services/auth/source/oauth2/jwtsigningkey_test.go
@@ -13,8 +13,8 @@ import (
"path/filepath"
"testing"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -30,7 +30,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
block, _ := pem.Decode(fileContent)
assert.NotNil(t, block)
- assert.EqualValues(t, "PRIVATE KEY", block.Type)
+ assert.Equal(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.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
+ assert.Equal(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.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
+ assert.Equal(t, 2048, rsaPrivateKey.N.BitLen())
})
})
@@ -62,7 +62,7 @@ func TestLoadOrCreateAsymmetricKey(t *testing.T) {
parsedKey := loadKey(t)
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
- assert.EqualValues(t, 3072, rsaPrivateKey.N.BitLen())
+ assert.Equal(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.EqualValues(t, 4096, rsaPrivateKey.N.BitLen())
+ assert.Equal(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.EqualValues(t, 256, ecdsaPrivateKey.Params().BitSize)
+ assert.Equal(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.EqualValues(t, 384, ecdsaPrivateKey.Params().BitSize)
+ assert.Equal(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.EqualValues(t, 521, ecdsaPrivateKey.Params().BitSize)
+ assert.Equal(t, 521, ecdsaPrivateKey.Params().BitSize)
})
t.Run("EdDSA", func(t *testing.T) {
diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go
index f2c1bb4894..773ce19c12 100644
--- a/services/auth/source/oauth2/providers.go
+++ b/services/auth/source/oauth2/providers.go
@@ -12,11 +12,11 @@ import (
"net/url"
"sort"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
"github.com/markbates/goth"
)
diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go
index 63318b84ef..1ef8d0af72 100644
--- a/services/auth/source/oauth2/providers_base.go
+++ b/services/auth/source/oauth2/providers_base.go
@@ -6,8 +6,8 @@ package oauth2
import (
"html/template"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/svg"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/svg"
)
// BaseProvider represents a common base for Provider
diff --git a/services/auth/source/oauth2/providers_custom.go b/services/auth/source/oauth2/providers_custom.go
index 65cf538ad7..51a412e0be 100644
--- a/services/auth/source/oauth2/providers_custom.go
+++ b/services/auth/source/oauth2/providers_custom.go
@@ -4,7 +4,7 @@
package oauth2
import (
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/setting"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/azureadv2"
diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go
index f606581271..7950506ab7 100644
--- a/services/auth/source/oauth2/providers_openid.go
+++ b/services/auth/source/oauth2/providers_openid.go
@@ -6,9 +6,9 @@ package oauth2
import (
"html/template"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/svg"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/svg"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/openidConnect"
diff --git a/services/auth/source/oauth2/providers_simple.go b/services/auth/source/oauth2/providers_simple.go
index e95323a62a..8e2c0a7700 100644
--- a/services/auth/source/oauth2/providers_simple.go
+++ b/services/auth/source/oauth2/providers_simple.go
@@ -4,7 +4,7 @@
package oauth2
import (
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/setting"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/azuread"
diff --git a/services/auth/source/oauth2/source.go b/services/auth/source/oauth2/source.go
index 3f8616c6ff..a3126cf353 100644
--- a/services/auth/source/oauth2/source.go
+++ b/services/auth/source/oauth2/source.go
@@ -6,8 +6,8 @@ package oauth2
import (
"strings"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/json"
+ "forgejo.org/models/auth"
+ "forgejo.org/modules/json"
)
// Source holds configuration for the OAuth2 login source.
@@ -29,6 +29,7 @@ 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/source_authenticate.go b/services/auth/source/oauth2/source_authenticate.go
index bbda35dee0..1efd7be02a 100644
--- a/services/auth/source/oauth2/source_authenticate.go
+++ b/services/auth/source/oauth2/source_authenticate.go
@@ -6,8 +6,8 @@ package oauth2
import (
"context"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/services/auth/source/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/services/auth/source/db"
)
// Authenticate falls back to the db authenticator
diff --git a/services/auth/source/oauth2/store.go b/services/auth/source/oauth2/store.go
index e031653119..d52581ea2d 100644
--- a/services/auth/source/oauth2/store.go
+++ b/services/auth/source/oauth2/store.go
@@ -8,8 +8,8 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/modules/log"
- session_module "code.gitea.io/gitea/modules/session"
+ "forgejo.org/modules/log"
+ session_module "forgejo.org/modules/session"
chiSession "code.forgejo.org/go-chi/session"
"github.com/gorilla/sessions"
diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go
index 3405619d3f..b060b6b746 100644
--- a/services/auth/source/oauth2/token.go
+++ b/services/auth/source/oauth2/token.go
@@ -4,10 +4,11 @@
package oauth2
import (
+ "errors"
"fmt"
"time"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/modules/timeutil"
"github.com/golang-jwt/jwt/v5"
)
@@ -51,12 +52,12 @@ func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) {
return nil, err
}
if !parsedToken.Valid {
- return nil, fmt.Errorf("invalid token")
+ return nil, errors.New("invalid token")
}
var token *Token
var ok bool
if token, ok = parsedToken.Claims.(*Token); !ok || !parsedToken.Valid {
- return nil, fmt.Errorf("invalid token")
+ return nil, errors.New("invalid token")
}
return token, nil
}
diff --git a/services/auth/source/pam/assert_interface_test.go b/services/auth/source/pam/assert_interface_test.go
index 8e7648b8d3..8c54b7e9e2 100644
--- a/services/auth/source/pam/assert_interface_test.go
+++ b/services/auth/source/pam/assert_interface_test.go
@@ -4,9 +4,9 @@
package pam_test
import (
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/auth/source/pam"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/services/auth"
+ "forgejo.org/services/auth/source/pam"
)
// This test file exists to assert that our Source exposes the interfaces that we expect
diff --git a/services/auth/source/pam/source.go b/services/auth/source/pam/source.go
index 96b182e185..e1dc83ba43 100644
--- a/services/auth/source/pam/source.go
+++ b/services/auth/source/pam/source.go
@@ -4,8 +4,8 @@
package pam
import (
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/json"
+ "forgejo.org/models/auth"
+ "forgejo.org/modules/json"
)
// __________ _____ _____
diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go
index 0df0b2bca1..6f3ffc2d9d 100644
--- a/services/auth/source/pam/source_authenticate.go
+++ b/services/auth/source/pam/source_authenticate.go
@@ -8,12 +8,12 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/auth/pam"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/validation"
+ "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/auth/pam"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/validation"
"github.com/google/uuid"
)
diff --git a/services/auth/source/remote/source.go b/services/auth/source/remote/source.go
index 4165858a56..effbabc7d0 100644
--- a/services/auth/source/remote/source.go
+++ b/services/auth/source/remote/source.go
@@ -4,8 +4,8 @@
package remote
import (
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/json"
+ "forgejo.org/models/auth"
+ "forgejo.org/modules/json"
)
type Source struct {
diff --git a/services/auth/source/smtp/assert_interface_test.go b/services/auth/source/smtp/assert_interface_test.go
index 6c9cde66e1..6826dae873 100644
--- a/services/auth/source/smtp/assert_interface_test.go
+++ b/services/auth/source/smtp/assert_interface_test.go
@@ -4,9 +4,9 @@
package smtp_test
import (
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/auth/source/smtp"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/services/auth"
+ "forgejo.org/services/auth/source/smtp"
)
// This test file exists to assert that our Source exposes the interfaces that we expect
diff --git a/services/auth/source/smtp/source.go b/services/auth/source/smtp/source.go
index 2a648e421e..d44971bab0 100644
--- a/services/auth/source/smtp/source.go
+++ b/services/auth/source/smtp/source.go
@@ -4,8 +4,8 @@
package smtp
import (
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/json"
+ "forgejo.org/models/auth"
+ "forgejo.org/modules/json"
)
// _________ __________________________
diff --git a/services/auth/source/smtp/source_authenticate.go b/services/auth/source/smtp/source_authenticate.go
index 1f0a61c789..3d7ccd0669 100644
--- a/services/auth/source/smtp/source_authenticate.go
+++ b/services/auth/source/smtp/source_authenticate.go
@@ -10,10 +10,10 @@ import (
"net/textproto"
"strings"
- auth_model "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/util"
+ auth_model "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/util"
)
// Authenticate queries if the provided login/password is authenticates against the SMTP server
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
index 3a2411ec55..46be6937fb 100644
--- a/services/auth/source/source_group_sync.go
+++ b/services/auth/source/source_group_sync.go
@@ -7,11 +7,11 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/organization"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/models"
+ "forgejo.org/models/organization"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
)
type syncType int
diff --git a/services/auth/sync.go b/services/auth/sync.go
index 7562ac812b..c594be7a24 100644
--- a/services/auth/sync.go
+++ b/services/auth/sync.go
@@ -6,9 +6,9 @@ package auth
import (
"context"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/log"
)
// SyncExternalUsers is used to synchronize users with external authorization source
diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go
index d3cc4c6fb1..0cdc113379 100644
--- a/services/automerge/automerge.go
+++ b/services/automerge/automerge.go
@@ -8,22 +8,22 @@ import (
"errors"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- pull_model "code.gitea.io/gitea/models/pull"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/queue"
- notify_service "code.gitea.io/gitea/services/notify"
- pull_service "code.gitea.io/gitea/services/pull"
- repo_service "code.gitea.io/gitea/services/repository"
- shared_automerge "code.gitea.io/gitea/services/shared/automerge"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ pull_model "forgejo.org/models/pull"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/queue"
+ notify_service "forgejo.org/services/notify"
+ pull_service "forgejo.org/services/pull"
+ repo_service "forgejo.org/services/repository"
+ shared_automerge "forgejo.org/services/shared/automerge"
)
// Init runs the task queue to that handles auto merges
@@ -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 fmt.Errorf("unable to create pr_auto_merge queue")
+ return errors.New("unable to create pr_auto_merge queue")
}
go graceful.GetManager().RunWithCancel(shared_automerge.PRAutoMergeQueue)
return nil
@@ -107,6 +107,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
return
}
if !exists {
+ log.Trace("GetScheduledMergeByPullID found nothing for PR %d", pullID)
return
}
@@ -161,7 +162,7 @@ func handlePullRequestAutoMerge(pullID int64, sha string) {
return
}
case issues_model.PullRequestFlowAGit:
- headBranchExist := git.IsReferenceExist(ctx, baseGitRepo.Path, pr.GetGitRefName())
+ headBranchExist := baseGitRepo.IsReferenceExist(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
@@ -204,6 +205,10 @@ 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/automerge/notify.go b/services/automerge/notify.go
index cb078214f6..3b5eae9d48 100644
--- a/services/automerge/notify.go
+++ b/services/automerge/notify.go
@@ -6,10 +6,10 @@ package automerge
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- notify_service "code.gitea.io/gitea/services/notify"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ notify_service "forgejo.org/services/notify"
)
type automergeNotifier struct {
diff --git a/services/context/access_log.go b/services/context/access_log.go
index 0926748ac5..7a54b746f6 100644
--- a/services/context/access_log.go
+++ b/services/context/access_log.go
@@ -12,10 +12,10 @@ import (
"text/template"
"time"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web/middleware"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web/middleware"
)
type routerLoggerOptions struct {
diff --git a/services/context/api.go b/services/context/api.go
index 871a2f012d..e9f67c720d 100644
--- a/services/context/api.go
+++ b/services/context/api.go
@@ -6,23 +6,24 @@ package context
import (
"context"
+ "errors"
"fmt"
"net/http"
"net/url"
"strings"
- issues_model "code.gitea.io/gitea/models/issues"
- quota_model "code.gitea.io/gitea/models/quota"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- mc "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/httpcache"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/web"
- web_types "code.gitea.io/gitea/modules/web/types"
+ issues_model "forgejo.org/models/issues"
+ quota_model "forgejo.org/models/quota"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ mc "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/httpcache"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/web"
+ web_types "forgejo.org/modules/web/types"
"code.forgejo.org/go-chi/cache"
)
@@ -186,7 +187,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 = ""
}
}
@@ -285,8 +286,8 @@ func APIContexter() func(http.Handler) http.Handler {
}
defer baseCleanUp()
- ctx.Base.AppendContextValue(apiContextKey, ctx)
- ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
+ ctx.AppendContextValue(apiContextKey, ctx)
+ ctx.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") {
@@ -334,7 +335,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
}
@@ -365,12 +366,12 @@ func RepoRefForAPI(next http.Handler) http.Handler {
ctx := GetAPIContext(req)
if ctx.Repo.Repository.IsEmpty {
- ctx.NotFound(fmt.Errorf("repository is empty"))
+ ctx.NotFound(errors.New("repository is empty"))
return
}
if ctx.Repo.GitRepo == nil {
- ctx.InternalServerError(fmt.Errorf("no open git repo"))
+ ctx.InternalServerError(errors.New("no open git repo"))
return
}
diff --git a/services/context/api_org.go b/services/context/api_org.go
index dad02b1719..acc9594e48 100644
--- a/services/context/api_org.go
+++ b/services/context/api_org.go
@@ -3,7 +3,7 @@
package context
-import "code.gitea.io/gitea/models/organization"
+import "forgejo.org/models/organization"
// APIOrganization contains organization and team
type APIOrganization struct {
diff --git a/services/context/api_test.go b/services/context/api_test.go
index 6064fee1c3..4bc89939ca 100644
--- a/services/context/api_test.go
+++ b/services/context/api_test.go
@@ -8,14 +8,15 @@ import (
"strconv"
"testing"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenAPILinks(t *testing.T) {
- setting.AppURL = "http://localhost:3000/"
+ defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")()
kases := map[string][]string{
"api/v1/repos/jerrykan/example-repo/issues?state=all": {
`; rel="next"`,
@@ -46,6 +47,6 @@ func TestGenAPILinks(t *testing.T) {
links := genAPILinks(u, 100, 20, curPage)
- assert.EqualValues(t, links, response)
+ assert.Equal(t, links, response)
}
}
diff --git a/services/context/base.go b/services/context/base.go
index 0259e0d806..dc3d226bb0 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -14,12 +14,12 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/httplib"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/web/middleware"
+ "forgejo.org/modules/httplib"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/translation"
+ "forgejo.org/modules/web/middleware"
"github.com/go-chi/chi/v5"
)
@@ -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 {
+ if len(status) == 1 && status[0] > 0 {
code = status[0]
}
diff --git a/services/context/base_test.go b/services/context/base_test.go
index 823f20e00b..9e058d8f24 100644
--- a/services/context/base_test.go
+++ b/services/context/base_test.go
@@ -8,12 +8,14 @@ import (
"net/http/httptest"
"testing"
- "code.gitea.io/gitea/modules/setting"
+ "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 {
@@ -34,6 +36,7 @@ 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)
@@ -45,3 +48,24 @@ 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/captcha.go b/services/context/captcha.go
index da837acb00..8ae8bdcae3 100644
--- a/services/context/captcha.go
+++ b/services/context/captcha.go
@@ -7,14 +7,14 @@ import (
"fmt"
"sync"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/hcaptcha"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/mcaptcha"
- "code.gitea.io/gitea/modules/recaptcha"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/turnstile"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/hcaptcha"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/mcaptcha"
+ "forgejo.org/modules/recaptcha"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/turnstile"
mc "code.forgejo.org/go-chi/cache"
"code.forgejo.org/go-chi/captcha"
diff --git a/services/context/context.go b/services/context/context.go
index 91e7b1849d..68074964c8 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -15,17 +15,17 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- mc "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/httpcache"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/modules/web/middleware"
- web_types "code.gitea.io/gitea/modules/web/types"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ mc "forgejo.org/modules/cache"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/httpcache"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/templates"
+ "forgejo.org/modules/translation"
+ "forgejo.org/modules/web"
+ "forgejo.org/modules/web/middleware"
+ web_types "forgejo.org/modules/web/types"
"code.forgejo.org/go-chi/cache"
"code.forgejo.org/go-chi/session"
@@ -41,7 +41,7 @@ type Render interface {
type Context struct {
*Base
- TemplateContext TemplateContext
+ TemplateContext *templates.Context
Render Render
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
@@ -64,8 +64,6 @@ 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)
@@ -98,10 +96,11 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
return ctx
}
-func NewTemplateContextForWeb(ctx *Context) TemplateContext {
- tmplCtx := NewTemplateContext(ctx)
- tmplCtx["Locale"] = ctx.Base.Locale
- tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
+func NewTemplateContextForWeb(ctx *Context) *templates.Context {
+ tmplCtx := templates.NewContext(ctx)
+ tmplCtx.Locale = ctx.Locale
+ tmplCtx.AvatarUtils = templates.NewAvatarUtils(ctx)
+ tmplCtx.Data = ctx.Data
return tmplCtx
}
@@ -121,6 +120,18 @@ 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()
@@ -151,8 +162,8 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData
- ctx.Base.AppendContextValue(WebContextKey, ctx)
- ctx.Base.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
+ ctx.AppendContextValue(WebContextKey, ctx)
+ ctx.AppendContextValueFunc(gitrepo.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })
ctx.Csrf = NewCSRFProtector(csrfOpts)
@@ -208,6 +219,25 @@ 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_cookie.go b/services/context/context_cookie.go
index 3699f81071..08ef84b5eb 100644
--- a/services/context/context_cookie.go
+++ b/services/context/context_cookie.go
@@ -7,11 +7,11 @@ import (
"net/http"
"strings"
- auth_model "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/web/middleware"
+ auth_model "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/web/middleware"
)
const CookieNameFlash = "gitea_flash"
diff --git a/services/context/context_model.go b/services/context/context_model.go
index 4f70aac516..1a8751ee63 100644
--- a/services/context/context_model.go
+++ b/services/context/context_model.go
@@ -4,7 +4,7 @@
package context
import (
- "code.gitea.io/gitea/models/unit"
+ "forgejo.org/models/unit"
)
// IsUserSiteAdmin returns true if current user is a site admin
diff --git a/services/context/context_response.go b/services/context/context_response.go
index f36b834a44..386bdd2652 100644
--- a/services/context/context_response.go
+++ b/services/context/context_response.go
@@ -1,4 +1,5 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
@@ -16,13 +17,14 @@ import (
"syscall"
"time"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/httplib"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/web/middleware"
+ "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/httplib"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/templates"
+ "forgejo.org/modules/web/middleware"
)
// RedirectToUser redirect to a differently-named user
@@ -66,7 +68,10 @@ func (ctx *Context) RedirectToFirst(location ...string) string {
return setting.AppSubURL + "/"
}
-const tplStatus500 base.TplName = "status/500"
+const (
+ tplStatus404 base.TplName = "status/404"
+ 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) {
@@ -125,6 +130,21 @@ 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)
@@ -152,9 +172,10 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
return
}
+ ctx.validateTwoFactorRequirement()
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
- ctx.Data["Title"] = "Page Not Found"
- ctx.HTML(http.StatusNotFound, base.TplName("status/404"))
+ ctx.Data["Title"] = ctx.Locale.TrString("error.not_found.title")
+ ctx.HTML(http.StatusNotFound, tplStatus404)
}
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
@@ -177,7 +198,7 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
}
}
- ctx.Data["Title"] = "Internal Server Error"
+ ctx.validateTwoFactorRequirement()
ctx.HTML(http.StatusInternalServerError, tplStatus500)
}
diff --git a/services/context/context_template.go b/services/context/context_template.go
deleted file mode 100644
index 7878d409ca..0000000000
--- a/services/context/context_template.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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/context_test.go b/services/context/context_test.go
index 033ce2ef0a..c2a271d2b7 100644
--- a/services/context/context_test.go
+++ b/services/context/context_test.go
@@ -8,7 +8,7 @@ import (
"net/http/httptest"
"testing"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/assert"
)
diff --git a/services/context/csrf.go b/services/context/csrf.go
index 51127c6eb0..82dd9283ff 100644
--- a/services/context/csrf.go
+++ b/services/context/csrf.go
@@ -25,8 +25,8 @@ import (
"strconv"
"time"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/util"
)
const (
diff --git a/services/context/org.go b/services/context/org.go
index 9673f2f5a9..c7d06b9bcc 100644
--- a/services/context/org.go
+++ b/services/context/org.go
@@ -1,5 +1,6 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2020 The Gitea Authors.
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
@@ -7,14 +8,15 @@ package context
import (
"strings"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ redirect_service "forgejo.org/services/redirect"
)
// Organization contains organization context
@@ -47,13 +49,13 @@ func GetOrganizationByParams(ctx *Context) {
ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
if err != nil {
if organization.IsErrOrgNotExist(err) {
- redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName)
+ redirectUserID, err := redirect_service.LookupUserRedirect(ctx, ctx.Doer, orgName)
if err == nil {
RedirectToUser(ctx.Base, orgName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", err)
} else {
- ctx.ServerError("LookupUserRedirect", err)
+ ctx.ServerError("LookupRedirect", err)
}
} else {
ctx.ServerError("GetUserByName", err)
@@ -165,6 +167,7 @@ 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 c452c657e7..50ffa8eb7c 100644
--- a/services/context/package.go
+++ b/services/context/package.go
@@ -7,14 +7,14 @@ import (
"fmt"
"net/http"
- "code.gitea.io/gitea/models/organization"
- packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/models/perm"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/templates"
+ "forgejo.org/models/organization"
+ packages_model "forgejo.org/models/packages"
+ "forgejo.org/models/perm"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/templates"
)
// Package contains owner, access mode and optional the package descriptor
@@ -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.IsActive || doer.ProhibitLogin) {
+ if doer != nil && !doer.IsGhost() && !doer.IsAccessAllowed(ctx) {
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.Base.AppendContextValue(WebContextKey, ctx)
+ ctx.AppendContextValue(WebContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
diff --git a/services/context/pagination.go b/services/context/pagination.go
index 655a278f9f..b826e59dea 100644
--- a/services/context/pagination.go
+++ b/services/context/pagination.go
@@ -9,7 +9,7 @@ import (
"net/url"
"strings"
- "code.gitea.io/gitea/modules/paginator"
+ "forgejo.org/modules/paginator"
)
// Pagination provides a pagination via paginator.Paginator and additional configurations for the link params used in rendering
diff --git a/services/context/permission.go b/services/context/permission.go
index 14a9801dcc..b6af87f912 100644
--- a/services/context/permission.go
+++ b/services/context/permission.go
@@ -6,10 +6,10 @@ package context
import (
"net/http"
- auth_model "code.gitea.io/gitea/models/auth"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/log"
+ auth_model "forgejo.org/models/auth"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/log"
)
// RequireRepoAdmin returns a middleware for requiring repository admin permission
diff --git a/services/context/private.go b/services/context/private.go
index 8b41949f60..94ee31876a 100644
--- a/services/context/private.go
+++ b/services/context/private.go
@@ -9,10 +9,10 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/web"
- web_types "code.gitea.io/gitea/modules/web/types"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/web"
+ web_types "forgejo.org/modules/web/types"
)
// PrivateContext represents a context for private routes
@@ -67,7 +67,7 @@ func PrivateContexter() func(http.Handler) http.Handler {
base, baseCleanUp := NewBaseContext(w, req)
ctx := &PrivateContext{Base: base}
defer baseCleanUp()
- ctx.Base.AppendContextValue(privateContextKey, ctx)
+ ctx.AppendContextValue(privateContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
diff --git a/services/context/quota.go b/services/context/quota.go
index 94e8847696..502a316107 100644
--- a/services/context/quota.go
+++ b/services/context/quota.go
@@ -8,8 +8,8 @@ import (
"net/http"
"strings"
- quota_model "code.gitea.io/gitea/models/quota"
- "code.gitea.io/gitea/modules/base"
+ quota_model "forgejo.org/models/quota"
+ "forgejo.org/modules/base"
)
type QuotaTargetType int
@@ -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.Base.originCtx, subject, userID, username, func(userID int64, username string) {
+ ok, err := checkQuota(ctx.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.Base.originCtx, subject, userID, username, func(userID int64, username string) {
+ ok, err := checkQuota(ctx.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 ff03844c03..e01e1ddafc 100644
--- a/services/context/repo.go
+++ b/services/context/repo.go
@@ -15,26 +15,27 @@ import (
"path"
"strings"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/card"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- code_indexer "code.gitea.io/gitea/modules/indexer/code"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ packages_model "forgejo.org/models/packages"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/card"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ code_indexer "forgejo.org/modules/indexer/code"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ repo_module "forgejo.org/modules/repository"
+ "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"
)
@@ -83,7 +84,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.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
+ return r.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
}
func (r *Repository) GetObjectFormat() git.ObjectFormat {
@@ -160,12 +161,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.Permission.CanWriteIssuesOrPulls(issue.IsPull) || issue.IsPoster(user.ID) || isAssigned)
+ r.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.Permission.CanWriteIssuesOrPulls(isPull)
+ return r.Repository.IsDependenciesEnabled(ctx) && r.CanWriteIssuesOrPulls(isPull)
}
// GetCommitsCount returns cached commit count for current view
@@ -361,7 +362,9 @@ func RedirectToRepo(ctx *Base, redirectRepoID int64) {
if ctx.Req.URL.RawQuery != "" {
redirectPath += "?" + ctx.Req.URL.RawQuery
}
- ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
+ // 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)
}
func repoAssignment(ctx *Context, repo *repo_model.Repository) {
@@ -378,7 +381,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
}
// Check access.
- if !ctx.Repo.Permission.HasAccess() {
+ if !ctx.Repo.HasAccess() {
if ctx.FormString("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx)
return
@@ -475,12 +478,12 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
return nil
}
- if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
+ if redirectUserID, err := redirect_service.LookupUserRedirect(ctx, ctx.Doer, userName); err == nil {
RedirectToUser(ctx.Base, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.NotFound("GetUserByName", nil)
} else {
- ctx.ServerError("LookupUserRedirect", err)
+ ctx.ServerError("LookupRedirect", err)
}
} else {
ctx.ServerError("GetUserByName", err)
@@ -517,7 +520,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 := repo_model.LookupRedirect(ctx, owner.ID, repoName)
+ redirectRepoID, err := redirect_service.LookupRepoRedirect(ctx, ctx.Doer, owner.ID, repoName)
if err == nil {
RedirectToRepo(ctx.Base, redirectRepoID)
} else if repo_model.IsErrRedirectNotExist(err) {
@@ -591,6 +594,7 @@ 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 {
@@ -641,7 +645,11 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
ctx.Data["OpenGraphImageURL"] = repo.SummaryCardURL()
ctx.Data["OpenGraphImageWidth"] = cardWidth
ctx.Data["OpenGraphImageHeight"] = cardHeight
- ctx.Data["OpenGraphImageAltText"] = ctx.Tr("repo.summary_card_alt", repo.FullName())
+ 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)
+ }
if repo.IsFork {
RetrieveBaseRepo(ctx, repo)
diff --git a/services/context/repository.go b/services/context/repository.go
index 422ac3f58d..7eef2c5068 100644
--- a/services/context/repository.go
+++ b/services/context/repository.go
@@ -6,7 +6,7 @@ package context
import (
"net/http"
- repo_model "code.gitea.io/gitea/models/repo"
+ repo_model "forgejo.org/models/repo"
)
// RepositoryIDAssignmentAPI returns a middleware to handle context-repo assignment for api routes
diff --git a/services/context/response.go b/services/context/response.go
index 2f271f211b..8fc631e671 100644
--- a/services/context/response.go
+++ b/services/context/response.go
@@ -6,7 +6,7 @@ package context
import (
"net/http"
- web_types "code.gitea.io/gitea/modules/web/types"
+ web_types "forgejo.org/modules/web/types"
)
// ResponseWriter represents a response writer for HTTP
diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go
index 77a7eb9377..e71fc50c1f 100644
--- a/services/context/upload/upload.go
+++ b/services/context/upload/upload.go
@@ -11,9 +11,9 @@ import (
"regexp"
"strings"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/context"
)
// ErrFileTypeForbidden not allowed file type error
@@ -76,14 +76,15 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error {
// AddUploadContext renders template values for dropzone
func AddUploadContext(ctx *context.Context, uploadType string) {
- if uploadType == "release" {
+ switch uploadType {
+ case "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
- } else if uploadType == "comment" {
+ case "comment":
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove"
if len(ctx.Params(":index")) > 0 {
@@ -94,7 +95,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
- } else if uploadType == "repo" {
+ case "repo":
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove"
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file"
diff --git a/services/context/user.go b/services/context/user.go
index 4c9cd2928b..63260a49aa 100644
--- a/services/context/user.go
+++ b/services/context/user.go
@@ -8,7 +8,8 @@ import (
"net/http"
"strings"
- user_model "code.gitea.io/gitea/models/user"
+ user_model "forgejo.org/models/user"
+ redirect_service "forgejo.org/services/redirect"
)
// UserAssignmentWeb returns a middleware to handle context-user assignment for web routes
@@ -68,12 +69,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 := user_model.LookupUserRedirect(ctx, username); err == nil {
+ if redirectUserID, err := redirect_service.LookupUserRedirect(ctx, doer, username); err == nil {
RedirectToUser(ctx, username, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
errCb(http.StatusNotFound, "GetUserByName", err)
} else {
- errCb(http.StatusInternalServerError, "LookupUserRedirect", err)
+ errCb(http.StatusInternalServerError, "LookupRedirect", err)
}
} else {
errCb(http.StatusInternalServerError, "GetUserByName", err)
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index 7c829f3598..a4e674a896 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -15,16 +15,16 @@ import (
"testing"
"time"
- org_model "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ org_model "forgejo.org/models/organization"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/templates"
+ "forgejo.org/modules/translation"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
@@ -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.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ ctx.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.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ ctx.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.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
+ ctx.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp
}
diff --git a/services/contexttest/pagedata_test.go b/services/contexttest/pagedata_test.go
new file mode 100644
index 0000000000..0c9319b6db
--- /dev/null
+++ b/services/contexttest/pagedata_test.go
@@ -0,0 +1,63 @@
+// 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
new file mode 100644
index 0000000000..703c1f1261
--- /dev/null
+++ b/services/convert/action.go
@@ -0,0 +1,49 @@
+// 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/activity.go b/services/convert/activity.go
index 01fef73e58..213db13772 100644
--- a/services/convert/activity.go
+++ b/services/convert/activity.go
@@ -6,12 +6,12 @@ package convert
import (
"context"
- activities_model "code.gitea.io/gitea/models/activities"
- perm_model "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
+ activities_model "forgejo.org/models/activities"
+ perm_model "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
)
func ToActivity(ctx context.Context, ac *activities_model.Action, doer *user_model.User) *api.Activity {
diff --git a/services/convert/activitypub_person.go b/services/convert/activitypub_person.go
new file mode 100644
index 0000000000..2c05f8c1c0
--- /dev/null
+++ b/services/convert/activitypub_person.go
@@ -0,0 +1,62 @@
+// 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
new file mode 100644
index 0000000000..9c62e6f25c
--- /dev/null
+++ b/services/convert/activitypub_user_action.go
@@ -0,0 +1,177 @@
+// 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 d632c94c18..74ae7c509c 100644
--- a/services/convert/attachment.go
+++ b/services/convert/attachment.go
@@ -4,8 +4,11 @@
package convert
import (
- repo_model "code.gitea.io/gitea/models/repo"
- api "code.gitea.io/gitea/modules/structs"
+ "mime"
+ "path/filepath"
+
+ repo_model "forgejo.org/models/repo"
+ api "forgejo.org/modules/structs"
)
func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
@@ -20,9 +23,13 @@ func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm
return attach.DownloadURL()
}
-// 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)
+// 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)),
+ }
}
// ToAPIAttachment converts models.Attachment to api.Attachment for API usage
diff --git a/services/convert/attachment_test.go b/services/convert/attachment_test.go
new file mode 100644
index 0000000000..d7bf0c1ee7
--- /dev/null
+++ b/services/convert/attachment_test.go
@@ -0,0 +1,56 @@
+// 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/convert.go b/services/convert/convert.go
index 7a094494e4..2ea24a1b51 100644
--- a/services/convert/convert.go
+++ b/services/convert/convert.go
@@ -11,24 +11,24 @@ import (
"strings"
"time"
- actions_model "code.gitea.io/gitea/models/actions"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/auth"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/gitdiff"
+ actions_model "forgejo.org/models/actions"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/auth"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/gitdiff"
)
// ToEmail convert models.EmailAddress to api.Email
diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go
index e0efcddbcb..6a691966b8 100644
--- a/services/convert/git_commit.go
+++ b/services/convert/git_commit.go
@@ -8,14 +8,13 @@ import (
"net/url"
"time"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- ctx "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/gitdiff"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ ctx "forgejo.org/services/context"
)
// ToCommitUser convert a git.Signature to an api.CommitUser
@@ -210,17 +209,15 @@ func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep
// Get diff stats for commit
if opts.Stat {
- diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{
- AfterCommitID: commit.ID.String(),
- })
+ _, totalAdditions, totalDeletions, err := gitRepo.GetCommitShortStat(commit.ID.String())
if err != nil {
return nil, err
}
res.Stats = &api.CommitStats{
- Total: diff.TotalAddition + diff.TotalDeletion,
- Additions: diff.TotalAddition,
- Deletions: diff.TotalDeletion,
+ Total: totalAdditions + totalDeletions,
+ Additions: totalAdditions,
+ Deletions: totalDeletions,
}
}
diff --git a/services/convert/git_commit_test.go b/services/convert/git_commit_test.go
index 68d1b05168..97dff365e6 100644
--- a/services/convert/git_commit_test.go
+++ b/services/convert/git_commit_test.go
@@ -7,11 +7,11 @@ import (
"testing"
"time"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -34,7 +34,7 @@ func TestToCommitMeta(t *testing.T) {
commitMeta := ToCommitMeta(headRepo, tag)
assert.NotNil(t, commitMeta)
- assert.EqualValues(t, &api.CommitMeta{
+ assert.Equal(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.go b/services/convert/issue.go
index f514dc4313..c7803794d0 100644
--- a/services/convert/issue.go
+++ b/services/convert/issue.go
@@ -9,13 +9,13 @@ import (
"net/url"
"strings"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/label"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/label"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
)
func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue {
diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go
index 9ec9ac7684..9ea315aee6 100644
--- a/services/convert/issue_comment.go
+++ b/services/convert/issue_comment.go
@@ -6,12 +6,12 @@ package convert
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
)
// ToAPIComment converts a issues_model.Comment to the api.Comment format for API usage
diff --git a/services/convert/issue_test.go b/services/convert/issue_test.go
index 0aeb3e5612..ea8ad9b7ef 100644
--- a/services/convert/issue_test.go
+++ b/services/convert/issue_test.go
@@ -8,12 +8,12 @@ import (
"testing"
"time"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/timeutil"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,10 +24,11 @@ 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",
- URL: fmt.Sprintf("%sapi/v1/repos/user2/repo1/labels/%d", setting.AppURL, label.ID),
+ 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),
}, ToLabel(label, repo, nil))
}
diff --git a/services/convert/main_test.go b/services/convert/main_test.go
index b28b8f9446..5915d16be4 100644
--- a/services/convert/main_test.go
+++ b/services/convert/main_test.go
@@ -6,10 +6,10 @@ package convert
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
)
func TestMain(m *testing.M) {
diff --git a/services/convert/mirror.go b/services/convert/mirror.go
index 85e0d1c856..5a815f3a5c 100644
--- a/services/convert/mirror.go
+++ b/services/convert/mirror.go
@@ -6,8 +6,8 @@ package convert
import (
"context"
- repo_model "code.gitea.io/gitea/models/repo"
- api "code.gitea.io/gitea/modules/structs"
+ repo_model "forgejo.org/models/repo"
+ api "forgejo.org/modules/structs"
)
// ToPushMirror convert from repo_model.PushMirror and remoteAddress to api.TopicResponse
@@ -23,5 +23,6 @@ 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 41063cf399..2a69b62e4b 100644
--- a/services/convert/notification.go
+++ b/services/convert/notification.go
@@ -7,17 +7,17 @@ import (
"context"
"net/url"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- api "code.gitea.io/gitea/modules/structs"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ api "forgejo.org/modules/structs"
)
// ToNotificationThread convert a Notification to api.NotificationThread
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/package.go b/services/convert/package.go
index b5fca21a3c..a28e60e1b1 100644
--- a/services/convert/package.go
+++ b/services/convert/package.go
@@ -6,10 +6,10 @@ package convert
import (
"context"
- "code.gitea.io/gitea/models/packages"
- access_model "code.gitea.io/gitea/models/perm/access"
- user_model "code.gitea.io/gitea/models/user"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/packages"
+ access_model "forgejo.org/models/perm/access"
+ user_model "forgejo.org/models/user"
+ api "forgejo.org/modules/structs"
)
// ToPackage convert a packages.PackageDescriptor to api.Package
diff --git a/services/convert/pull.go b/services/convert/pull.go
index 70dc22445a..ca965a0d18 100644
--- a/services/convert/pull.go
+++ b/services/convert/pull.go
@@ -7,15 +7,15 @@ import (
"context"
"fmt"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
)
// ToAPIPullRequest assumes following fields have been assigned with valid values:
@@ -66,33 +66,36 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
}
apiPullRequest := &api.PullRequest{
- ID: pr.ID,
- URL: pr.Issue.HTMLURL(),
- Index: pr.Index,
- Poster: apiIssue.Poster,
- Title: apiIssue.Title,
- Body: apiIssue.Body,
- Labels: apiIssue.Labels,
- Milestone: apiIssue.Milestone,
- Assignee: apiIssue.Assignee,
- Assignees: apiIssue.Assignees,
- State: apiIssue.State,
- Draft: pr.IsWorkInProgress(ctx),
- IsLocked: apiIssue.IsLocked,
- Comments: apiIssue.Comments,
- ReviewComments: pr.GetReviewCommentsCount(ctx),
- HTMLURL: pr.Issue.HTMLURL(),
- DiffURL: pr.Issue.DiffURL(),
- PatchURL: pr.Issue.PatchURL(),
- HasMerged: pr.HasMerged,
- MergeBase: pr.MergeBase,
- Mergeable: pr.Mergeable(ctx),
- Deadline: apiIssue.Deadline,
- Created: pr.Issue.CreatedUnix.AsTimePtr(),
- Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
- PinOrder: apiIssue.PinOrder,
+ ID: pr.ID,
+ URL: pr.Issue.HTMLURL(),
+ Index: pr.Index,
+ Poster: apiIssue.Poster,
+ Title: apiIssue.Title,
+ Body: apiIssue.Body,
+ Labels: apiIssue.Labels,
+ Milestone: apiIssue.Milestone,
+ Assignee: apiIssue.Assignee,
+ Assignees: apiIssue.Assignees,
+ State: apiIssue.State,
+ Draft: pr.IsWorkInProgress(ctx),
+ IsLocked: apiIssue.IsLocked,
+ Comments: apiIssue.Comments,
+ ReviewComments: pr.GetReviewCommentsCount(ctx),
+ HTMLURL: pr.Issue.HTMLURL(),
+ DiffURL: pr.Issue.DiffURL(),
+ PatchURL: pr.Issue.PatchURL(),
+ HasMerged: pr.HasMerged,
+ MergeBase: pr.MergeBase,
+ Mergeable: pr.Mergeable(ctx),
+ Deadline: apiIssue.Deadline,
+ Created: pr.Issue.CreatedUnix.AsTimePtr(),
+ Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
+ PinOrder: apiIssue.PinOrder,
+ RequestedReviewers: []*api.User{},
+ RequestedReviewersTeams: []*api.Team{},
AllowMaintainerEdit: pr.AllowMaintainerEdit,
+ Flow: int64(pr.Flow),
Base: &api.PRBranchInfo{
Name: pr.BaseBranch,
diff --git a/services/convert/pull_review.go b/services/convert/pull_review.go
index f7990e7a5c..97be118a83 100644
--- a/services/convert/pull_review.go
+++ b/services/convert/pull_review.go
@@ -7,9 +7,9 @@ import (
"context"
"strings"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- api "code.gitea.io/gitea/modules/structs"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ api "forgejo.org/modules/structs"
)
// ToPullReview convert a review to api format
@@ -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 1339ed5cc0..c0c69fd9ad 100644
--- a/services/convert/pull_test.go
+++ b/services/convert/pull_test.go
@@ -6,15 +6,15 @@ package convert
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.EqualValues(t, &structs.PRBranchInfo{
+ assert.Equal(t, &structs.PRBranchInfo{
Name: "branch1",
Ref: "refs/pull/2/head",
Sha: "4a357436d925b5c974181ff12a994538ddc5a269",
diff --git a/services/convert/quota.go b/services/convert/quota.go
index 791cd8e038..ba729feaac 100644
--- a/services/convert/quota.go
+++ b/services/convert/quota.go
@@ -7,12 +7,12 @@ import (
"context"
"strconv"
- action_model "code.gitea.io/gitea/models/actions"
- issue_model "code.gitea.io/gitea/models/issues"
- package_model "code.gitea.io/gitea/models/packages"
- quota_model "code.gitea.io/gitea/models/quota"
- repo_model "code.gitea.io/gitea/models/repo"
- api "code.gitea.io/gitea/modules/structs"
+ action_model "forgejo.org/models/actions"
+ issue_model "forgejo.org/models/issues"
+ package_model "forgejo.org/models/packages"
+ quota_model "forgejo.org/models/quota"
+ repo_model "forgejo.org/models/repo"
+ api "forgejo.org/modules/structs"
)
func ToQuotaRuleInfo(rule quota_model.Rule, withName bool) api.QuotaRuleInfo {
diff --git a/services/convert/release.go b/services/convert/release.go
index 8c0f61b56c..7773cf3b19 100644
--- a/services/convert/release.go
+++ b/services/convert/release.go
@@ -6,8 +6,8 @@ package convert
import (
"context"
- repo_model "code.gitea.io/gitea/models/repo"
- api "code.gitea.io/gitea/modules/structs"
+ repo_model "forgejo.org/models/repo"
+ api "forgejo.org/modules/structs"
)
// ToAPIRelease convert a repo_model.Release to api.Release
diff --git a/services/convert/release_test.go b/services/convert/release_test.go
index 2e40bb9cdd..1d214f0222 100644
--- a/services/convert/release_test.go
+++ b/services/convert/release_test.go
@@ -6,9 +6,9 @@ package convert
import (
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.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)
+ 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)
}
diff --git a/services/convert/repository.go b/services/convert/repository.go
index e4b2c7b8bc..1b0f46b3da 100644
--- a/services/convert/repository.go
+++ b/services/convert/repository.go
@@ -7,14 +7,14 @@ import (
"context"
"time"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
)
// ToRepo converts a Repository to api.Repository
diff --git a/services/convert/status.go b/services/convert/status.go
index 6cef63c1cd..1a71e70a52 100644
--- a/services/convert/status.go
+++ b/services/convert/status.go
@@ -6,9 +6,9 @@ package convert
import (
"context"
- git_model "code.gitea.io/gitea/models/git"
- user_model "code.gitea.io/gitea/models/user"
- api "code.gitea.io/gitea/modules/structs"
+ git_model "forgejo.org/models/git"
+ user_model "forgejo.org/models/user"
+ api "forgejo.org/modules/structs"
)
// ToCommitStatus converts git_model.CommitStatus to api.CommitStatus
diff --git a/services/convert/user.go b/services/convert/user.go
index 7b6775dfb4..444089fd83 100644
--- a/services/convert/user.go
+++ b/services/convert/user.go
@@ -6,9 +6,9 @@ package convert
import (
"context"
- "code.gitea.io/gitea/models/perm"
- user_model "code.gitea.io/gitea/models/user"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/perm"
+ user_model "forgejo.org/models/user"
+ api "forgejo.org/modules/structs"
)
// ToUser convert user_model.User to api.User
diff --git a/services/convert/user_test.go b/services/convert/user_test.go
index 0f0b520c9b..8a42a9d97d 100644
--- a/services/convert/user_test.go
+++ b/services/convert/user_test.go
@@ -6,10 +6,10 @@ package convert
import (
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ api "forgejo.org/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -31,11 +31,11 @@ func TestUser_ToUser(t *testing.T) {
apiUser = toUser(db.DefaultContext, user1, false, false)
assert.False(t, apiUser.IsAdmin)
- assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility)
+ assert.Equal(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.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
+ assert.Equal(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
}
diff --git a/services/convert/utils.go b/services/convert/utils.go
index fe35fd2dac..70c5f5cc18 100644
--- a/services/convert/utils.go
+++ b/services/convert/utils.go
@@ -7,8 +7,8 @@ package convert
import (
"strings"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
)
// ToCorrectPageSize makes sure page size is in allowed range.
@@ -38,6 +38,8 @@ 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 b464d8bb68..6c3bf7d938 100644
--- a/services/convert/utils_test.go
+++ b/services/convert/utils_test.go
@@ -10,10 +10,10 @@ import (
)
func TestToCorrectPageSize(t *testing.T) {
- assert.EqualValues(t, 30, ToCorrectPageSize(0))
- assert.EqualValues(t, 30, ToCorrectPageSize(-10))
- assert.EqualValues(t, 20, ToCorrectPageSize(20))
- assert.EqualValues(t, 50, ToCorrectPageSize(100))
+ assert.Equal(t, 30, ToCorrectPageSize(0))
+ assert.Equal(t, 30, ToCorrectPageSize(-10))
+ assert.Equal(t, 20, ToCorrectPageSize(20))
+ assert.Equal(t, 50, ToCorrectPageSize(100))
}
func TestToGitServiceType(t *testing.T) {
diff --git a/services/convert/wiki.go b/services/convert/wiki.go
index 767bfdb88d..adcbd52949 100644
--- a/services/convert/wiki.go
+++ b/services/convert/wiki.go
@@ -6,8 +6,8 @@ package convert
import (
"time"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
)
// ToWikiCommit convert a git commit into a WikiCommit
diff --git a/services/cron/cron.go b/services/cron/cron.go
index 3c5737e371..d020f3fd6c 100644
--- a/services/cron/cron.go
+++ b/services/cron/cron.go
@@ -9,10 +9,10 @@ import (
"runtime/pprof"
"time"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/sync"
- "code.gitea.io/gitea/modules/translation"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/sync"
+ "forgejo.org/modules/translation"
"github.com/go-co-op/gocron"
)
diff --git a/services/cron/setting.go b/services/cron/setting.go
index 6dad88830a..2db6c15370 100644
--- a/services/cron/setting.go
+++ b/services/cron/setting.go
@@ -6,7 +6,7 @@ package cron
import (
"time"
- "code.gitea.io/gitea/modules/translation"
+ "forgejo.org/modules/translation"
)
// Config represents a basic configuration interface that cron task
@@ -46,6 +46,13 @@ 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.go b/services/cron/tasks.go
index f8a7444c49..b547acdf05 100644
--- a/services/cron/tasks.go
+++ b/services/cron/tasks.go
@@ -11,14 +11,14 @@ import (
"sync"
"time"
- "code.gitea.io/gitea/models/db"
- system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation"
+ "forgejo.org/models/db"
+ system_model "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
)
var (
diff --git a/services/cron/tasks_actions.go b/services/cron/tasks_actions.go
index 59cfe36d14..2cd484fa69 100644
--- a/services/cron/tasks_actions.go
+++ b/services/cron/tasks_actions.go
@@ -5,10 +5,11 @@ package cron
import (
"context"
+ "time"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- actions_service "code.gitea.io/gitea/services/actions"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ actions_service "forgejo.org/services/actions"
)
func initActionsTasks() {
@@ -20,6 +21,7 @@ func initActionsTasks() {
registerCancelAbandonedJobs()
registerScheduleTasks()
registerActionsCleanup()
+ registerOfflineRunnersCleanup()
}
func registerStopZombieTasks() {
@@ -74,3 +76,22 @@ 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_basic.go b/services/cron/tasks_basic.go
index 23eb0dd291..5ada7a8f5c 100644
--- a/services/cron/tasks_basic.go
+++ b/services/cron/tasks_basic.go
@@ -7,18 +7,18 @@ import (
"context"
"time"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/migrations"
- mirror_service "code.gitea.io/gitea/services/mirror"
- packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
- repo_service "code.gitea.io/gitea/services/repository"
- archiver_service "code.gitea.io/gitea/services/repository/archiver"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/auth"
+ "forgejo.org/services/migrations"
+ mirror_service "forgejo.org/services/mirror"
+ packages_cleanup_service "forgejo.org/services/packages/cleanup"
+ repo_service "forgejo.org/services/repository"
+ archiver_service "forgejo.org/services/repository/archiver"
)
func registerUpdateMirrorTask() {
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
index e1ba5274e6..5cc5d4eb14 100644
--- a/services/cron/tasks_extended.go
+++ b/services/cron/tasks_extended.go
@@ -7,17 +7,18 @@ import (
"context"
"time"
- activities_model "code.gitea.io/gitea/models/activities"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/updatechecker"
- repo_service "code.gitea.io/gitea/services/repository"
- archiver_service "code.gitea.io/gitea/services/repository/archiver"
- user_service "code.gitea.io/gitea/services/user"
+ activities_model "forgejo.org/models/activities"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ 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"
)
func registerDeleteInactiveUsers() {
@@ -173,34 +174,35 @@ 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{
- 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,
+ 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 GC things that haven't been looked at in the past 3 days
LastUpdatedMoreThanAgo: 24 * time.Hour * 3,
NumberToCheckPerRepo: 100,
@@ -225,6 +227,24 @@ 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()
@@ -240,4 +260,7 @@ 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
new file mode 100644
index 0000000000..0b05ff9918
--- /dev/null
+++ b/services/cron/tasks_extended_test.go
@@ -0,0 +1,52 @@
+// 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/actions.go b/services/doctor/actions.go
index 7c44fb8392..c382132265 100644
--- a/services/doctor/actions.go
+++ b/services/doctor/actions.go
@@ -7,12 +7,12 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- unit_model "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ unit_model "forgejo.org/models/unit"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ repo_service "forgejo.org/services/repository"
)
func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error {
diff --git a/services/doctor/authorizedkeys.go b/services/doctor/authorizedkeys.go
index 2920cf51d7..465a3fc7c0 100644
--- a/services/doctor/authorizedkeys.go
+++ b/services/doctor/authorizedkeys.go
@@ -7,15 +7,16 @@ import (
"bufio"
"bytes"
"context"
+ "errors"
"fmt"
"os"
"path/filepath"
"strings"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
const tplCommentPrefix = `# gitea public key`
@@ -77,7 +78,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 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"`)
+ 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"`)
}
logger.Warn("authorized_keys is out of date. Attempting rewrite...")
err = asymkey_model.RewriteAllPublicKeys(ctx)
diff --git a/services/doctor/breaking.go b/services/doctor/breaking.go
index ec8433b8de..339f8e847c 100644
--- a/services/doctor/breaking.go
+++ b/services/doctor/breaking.go
@@ -7,11 +7,11 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/validation"
+ "forgejo.org/models/db"
+ "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/validation"
"xorm.io/builder"
)
diff --git a/services/doctor/checkOldArchives.go b/services/doctor/checkOldArchives.go
index 390dfb43aa..301e99391b 100644
--- a/services/doctor/checkOldArchives.go
+++ b/services/doctor/checkOldArchives.go
@@ -8,9 +8,9 @@ import (
"os"
"path/filepath"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/util"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/util"
)
func checkOldArchives(ctx context.Context, logger log.Logger, autofix bool) error {
diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go
index 9e2fcb645f..6fe4c9c5e6 100644
--- a/services/doctor/dbconsistency.go
+++ b/services/doctor/dbconsistency.go
@@ -6,16 +6,16 @@ package doctor
import (
"context"
- actions_model "code.gitea.io/gitea/models/actions"
- activities_model "code.gitea.io/gitea/models/activities"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/migrations"
- org_model "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ actions_model "forgejo.org/models/actions"
+ activities_model "forgejo.org/models/activities"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/migrations"
+ org_model "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
type consistencyCheck struct {
@@ -78,7 +78,14 @@ 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
- if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
+ 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 {
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 2a102b2194..c0ff22915d 100644
--- a/services/doctor/dbversion.go
+++ b/services/doctor/dbversion.go
@@ -6,14 +6,18 @@ package doctor
import (
"context"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/migrations"
- "code.gitea.io/gitea/modules/log"
+ "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, migrations.EnsureUpToDate); err != nil {
+ if err := db.InitEngineWithMigration(ctx, func(eng db.Engine) error {
+ return migrations.EnsureUpToDate(eng.(*xorm.Engine))
+ }); err != nil {
if !autofix {
logger.Critical("Error: %v during ensure up to date", err)
return err
@@ -21,7 +25,9 @@ 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, migrations.Migrate)
+ err = db.InitEngineWithMigration(ctx, func(eng db.Engine) error {
+ return migrations.Migrate(eng.(*xorm.Engine))
+ })
if err != nil {
logger.Critical("Error: %v during migration", err)
}
diff --git a/services/doctor/doctor.go b/services/doctor/doctor.go
index a4eb5e16b9..6d8e168bf2 100644
--- a/services/doctor/doctor.go
+++ b/services/doctor/doctor.go
@@ -10,11 +10,11 @@ import (
"sort"
"strings"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
)
// Check represents a Doctor check
diff --git a/services/doctor/fix16961.go b/services/doctor/fix16961.go
index 50d9ac6621..2212d9e903 100644
--- a/services/doctor/fix16961.go
+++ b/services/doctor/fix16961.go
@@ -9,12 +9,12 @@ import (
"errors"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/timeutil"
"xorm.io/builder"
)
diff --git a/services/doctor/fix16961_test.go b/services/doctor/fix16961_test.go
index 498ed9c8d5..75f9f206ab 100644
--- a/services/doctor/fix16961_test.go
+++ b/services/doctor/fix16961_test.go
@@ -6,7 +6,7 @@ package doctor
import (
"testing"
- repo_model "code.gitea.io/gitea/models/repo"
+ repo_model "forgejo.org/models/repo"
"github.com/stretchr/testify/assert"
)
@@ -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.EqualValues(t, &tt.expected, cfg)
+ assert.Equal(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.EqualValues(t, &tt.expected, cfg)
+ assert.Equal(t, &tt.expected, cfg)
})
}
}
diff --git a/services/doctor/fix8312.go b/services/doctor/fix8312.go
index 4fc049873a..31cd6686d7 100644
--- a/services/doctor/fix8312.go
+++ b/services/doctor/fix8312.go
@@ -6,11 +6,11 @@ package doctor
import (
"context"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- org_model "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ org_model "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ "forgejo.org/modules/log"
"xorm.io/builder"
)
diff --git a/services/doctor/heads.go b/services/doctor/heads.go
index 41fca01d57..7f9d1c73e8 100644
--- a/services/doctor/heads.go
+++ b/services/doctor/heads.go
@@ -6,9 +6,9 @@ package doctor
import (
"context"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
)
func synchronizeRepoHeads(ctx context.Context, logger log.Logger, autofix bool) error {
diff --git a/services/doctor/lfs.go b/services/doctor/lfs.go
index 8531b7bbe8..fe858605f4 100644
--- a/services/doctor/lfs.go
+++ b/services/doctor/lfs.go
@@ -5,12 +5,12 @@ package doctor
import (
"context"
- "fmt"
+ "errors"
"time"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/repository"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/repository"
)
func init() {
@@ -27,7 +27,7 @@ func init() {
func garbageCollectLFSCheck(ctx context.Context, logger log.Logger, autofix bool) error {
if !setting.LFS.StartServer {
- return fmt.Errorf("LFS support is disabled")
+ return errors.New("LFS support is disabled")
}
if err := repository.GarbageCollectLFSMetaObjects(ctx, repository.GarbageCollectLFSMetaObjectsOptions{
diff --git a/services/doctor/mergebase.go b/services/doctor/mergebase.go
index de460c4190..bebde30bee 100644
--- a/services/doctor/mergebase.go
+++ b/services/doctor/mergebase.go
@@ -8,11 +8,11 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
"xorm.io/builder"
)
diff --git a/services/doctor/misc.go b/services/doctor/misc.go
index 9300c3a25c..9b9c96b52b 100644
--- a/services/doctor/misc.go
+++ b/services/doctor/misc.go
@@ -11,17 +11,17 @@ import (
"path"
"strings"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
lru "github.com/hashicorp/golang-lru/v2"
"xorm.io/builder"
diff --git a/services/doctor/packages_nuget.go b/services/doctor/packages_nuget.go
index 47fdb3ac12..f6a33db779 100644
--- a/services/doctor/packages_nuget.go
+++ b/services/doctor/packages_nuget.go
@@ -9,12 +9,12 @@ import (
"slices"
"strings"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/log"
- packages_module "code.gitea.io/gitea/modules/packages"
- nuget_module "code.gitea.io/gitea/modules/packages/nuget"
- packages_service "code.gitea.io/gitea/services/packages"
+ "forgejo.org/models/db"
+ "forgejo.org/models/packages"
+ "forgejo.org/modules/log"
+ packages_module "forgejo.org/modules/packages"
+ nuget_module "forgejo.org/modules/packages/nuget"
+ packages_service "forgejo.org/services/packages"
"xorm.io/builder"
)
diff --git a/services/doctor/paths.go b/services/doctor/paths.go
index 8e37f01ef5..4fbe19ea04 100644
--- a/services/doctor/paths.go
+++ b/services/doctor/paths.go
@@ -8,8 +8,8 @@ import (
"fmt"
"os"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
type configurationFile struct {
diff --git a/services/doctor/push_mirror_consistency.go b/services/doctor/push_mirror_consistency.go
index 68b96d6415..07986770b2 100644
--- a/services/doctor/push_mirror_consistency.go
+++ b/services/doctor/push_mirror_consistency.go
@@ -7,9 +7,9 @@ import (
"context"
"strings"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/log"
"xorm.io/builder"
)
diff --git a/services/doctor/repository.go b/services/doctor/repository.go
index 6c33426636..cd51483d88 100644
--- a/services/doctor/repository.go
+++ b/services/doctor/repository.go
@@ -6,11 +6,11 @@ package doctor
import (
"context"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/storage"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/storage"
+ repo_service "forgejo.org/services/repository"
"xorm.io/builder"
)
diff --git a/services/doctor/storage.go b/services/doctor/storage.go
index 3f3b562c37..7dbe475d6c 100644
--- a/services/doctor/storage.go
+++ b/services/doctor/storage.go
@@ -9,16 +9,16 @@ import (
"io/fs"
"strings"
- "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- packages_module "code.gitea.io/gitea/modules/packages"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models/git"
+ "forgejo.org/models/packages"
+ "forgejo.org/models/repo"
+ "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ packages_module "forgejo.org/modules/packages"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/util"
)
type commonStorageCheckOptions struct {
diff --git a/services/doctor/usertype.go b/services/doctor/usertype.go
index ab32b78e62..0a034d8f9d 100644
--- a/services/doctor/usertype.go
+++ b/services/doctor/usertype.go
@@ -6,8 +6,8 @@ package doctor
import (
"context"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
)
func checkUserType(ctx context.Context, logger log.Logger, autofix bool) error {
diff --git a/services/externalaccount/link.go b/services/externalaccount/link.go
index d6e2ea7e94..5672313181 100644
--- a/services/externalaccount/link.go
+++ b/services/externalaccount/link.go
@@ -5,9 +5,9 @@ package externalaccount
import (
"context"
- "fmt"
+ "errors"
- user_model "code.gitea.io/gitea/models/user"
+ user_model "forgejo.org/models/user"
"github.com/markbates/goth"
)
@@ -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 fmt.Errorf("not in LinkAccount session")
+ return errors.New("not in LinkAccount session")
}
return LinkAccountToUser(ctx, user, gothUser.(goth.User))
diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go
index 3cfd8c81f9..68d085f6d0 100644
--- a/services/externalaccount/user.go
+++ b/services/externalaccount/user.go
@@ -8,11 +8,11 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models/auth"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/auth"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/structs"
"github.com/markbates/goth"
)
diff --git a/services/f3/driver/asset.go b/services/f3/driver/asset.go
deleted file mode 100644
index 61e571d1b6..0000000000
--- a/services/f3/driver/asset.go
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright Earl Warren
-// Copyright Loïc Dachary
-// SPDX-License-Identifier: MIT
-
-package driver
-
-import (
- "context"
- "crypto/sha256"
- "encoding/hex"
- "fmt"
- "io"
- "os"
-
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/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
deleted file mode 100644
index 88a3979713..0000000000
--- a/services/f3/driver/assets.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright Earl Warren
-// Copyright Loïc Dachary
-// SPDX-License-Identifier: MIT
-
-package driver
-
-import (
- "context"
- "fmt"
-
- repo_model "code.gitea.io/gitea/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
new file mode 100644
index 0000000000..64c188d6e0
--- /dev/null
+++ b/services/f3/driver/attachment.go
@@ -0,0 +1,185 @@
+// 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
new file mode 100644
index 0000000000..392afda52c
--- /dev/null
+++ b/services/f3/driver/attachments.go
@@ -0,0 +1,79 @@
+// 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/comment.go b/services/f3/driver/comment.go
index 166bfcd328..bd924930b5 100644
--- a/services/f3/driver/comment.go
+++ b/services/f3/driver/comment.go
@@ -8,10 +8,10 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/timeutil"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/comments.go b/services/f3/driver/comments.go
index eb79b74066..d8c84e290c 100644
--- a/services/f3/driver/comments.go
+++ b/services/f3/driver/comments.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/forge.go b/services/f3/driver/forge.go
index c232882753..03acb41450 100644
--- a/services/f3/driver/forge.go
+++ b/services/f3/driver/forge.go
@@ -8,7 +8,7 @@ import (
"context"
"fmt"
- user_model "code.gitea.io/gitea/models/user"
+ user_model "forgejo.org/models/user"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/issue.go b/services/f3/driver/issue.go
index 7e10f3a9db..6308c4cc2d 100644
--- a/services/f3/driver/issue.go
+++ b/services/f3/driver/issue.go
@@ -8,13 +8,13 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/timeutil"
- issue_service "code.gitea.io/gitea/services/issue"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/timeutil"
+ issue_service "forgejo.org/services/issue"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/issues.go b/services/f3/driver/issues.go
index 3a5a64e2b1..dd6828dc86 100644
--- a/services/f3/driver/issues.go
+++ b/services/f3/driver/issues.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/label.go b/services/f3/driver/label.go
index 509a69cf71..707ac2bab3 100644
--- a/services/f3/driver/label.go
+++ b/services/f3/driver/label.go
@@ -9,8 +9,8 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/labels.go b/services/f3/driver/labels.go
index 03f986b57a..4f705ed206 100644
--- a/services/f3/driver/labels.go
+++ b/services/f3/driver/labels.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/main.go b/services/f3/driver/main.go
index 825d456692..eb6e4a6fb6 100644
--- a/services/f3/driver/main.go
+++ b/services/f3/driver/main.go
@@ -5,7 +5,7 @@
package driver
import (
- driver_options "code.gitea.io/gitea/services/f3/driver/options"
+ driver_options "forgejo.org/services/f3/driver/options"
"code.forgejo.org/f3/gof3/v3/options"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
diff --git a/services/f3/driver/main_test.go b/services/f3/driver/main_test.go
index 8505b69b7e..b136fd5b23 100644
--- a/services/f3/driver/main_test.go
+++ b/services/f3/driver/main_test.go
@@ -7,14 +7,14 @@ package driver
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
- driver_options "code.gitea.io/gitea/services/f3/driver/options"
+ "forgejo.org/models/unittest"
+ driver_options "forgejo.org/services/f3/driver/options"
- _ "code.gitea.io/gitea/models"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/activities"
- _ "code.gitea.io/gitea/models/perm/access"
- _ "code.gitea.io/gitea/services/f3/driver/tests"
+ _ "forgejo.org/models"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/activities"
+ _ "forgejo.org/models/perm/access"
+ _ "forgejo.org/services/f3/driver/tests"
tests_f3 "code.forgejo.org/f3/gof3/v3/tree/tests/f3"
"github.com/stretchr/testify/require"
diff --git a/services/f3/driver/milestone.go b/services/f3/driver/milestone.go
index e57fee95a7..d10e6918ac 100644
--- a/services/f3/driver/milestone.go
+++ b/services/f3/driver/milestone.go
@@ -9,10 +9,10 @@ import (
"fmt"
"time"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/milestones.go b/services/f3/driver/milestones.go
index c816903bb1..cf0b70c158 100644
--- a/services/f3/driver/milestones.go
+++ b/services/f3/driver/milestones.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/options.go b/services/f3/driver/options.go
index abc5015dd0..516f9baf7a 100644
--- a/services/f3/driver/options.go
+++ b/services/f3/driver/options.go
@@ -7,7 +7,7 @@ package driver
import (
"net/http"
- driver_options "code.gitea.io/gitea/services/f3/driver/options"
+ driver_options "forgejo.org/services/f3/driver/options"
"code.forgejo.org/f3/gof3/v3/options"
)
diff --git a/services/f3/driver/organization.go b/services/f3/driver/organization.go
index 8e818a231a..af1eea4dda 100644
--- a/services/f3/driver/organization.go
+++ b/services/f3/driver/organization.go
@@ -8,9 +8,9 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- org_model "code.gitea.io/gitea/models/organization"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ org_model "forgejo.org/models/organization"
+ user_model "forgejo.org/models/user"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/organizations.go b/services/f3/driver/organizations.go
index adebdbbe95..eca6bfb9d4 100644
--- a/services/f3/driver/organizations.go
+++ b/services/f3/driver/organizations.go
@@ -8,9 +8,9 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- org_model "code.gitea.io/gitea/models/organization"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ org_model "forgejo.org/models/organization"
+ user_model "forgejo.org/models/user"
f3_id "code.forgejo.org/f3/gof3/v3/id"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
diff --git a/services/f3/driver/project.go b/services/f3/driver/project.go
index 2400663426..5a3ec81e40 100644
--- a/services/f3/driver/project.go
+++ b/services/f3/driver/project.go
@@ -9,9 +9,9 @@ import (
"fmt"
"strings"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- repo_service "code.gitea.io/gitea/services/repository"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ repo_service "forgejo.org/services/repository"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/projects.go b/services/f3/driver/projects.go
index fb447f3f01..0c76854f43 100644
--- a/services/f3/driver/projects.go
+++ b/services/f3/driver/projects.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
f3_id "code.forgejo.org/f3/gof3/v3/id"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
diff --git a/services/f3/driver/pullrequest.go b/services/f3/driver/pullrequest.go
index b8cb06c4d5..664ee6b13b 100644
--- a/services/f3/driver/pullrequest.go
+++ b/services/f3/driver/pullrequest.go
@@ -9,13 +9,13 @@ import (
"fmt"
"time"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/timeutil"
- issue_service "code.gitea.io/gitea/services/issue"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/timeutil"
+ issue_service "forgejo.org/services/issue"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/pullrequests.go b/services/f3/driver/pullrequests.go
index e7f2910314..227171994c 100644
--- a/services/f3/driver/pullrequests.go
+++ b/services/f3/driver/pullrequests.go
@@ -8,9 +8,9 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/optional"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/optional"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/reaction.go b/services/f3/driver/reaction.go
index 4f12fa41db..b959206074 100644
--- a/services/f3/driver/reaction.go
+++ b/services/f3/driver/reaction.go
@@ -8,9 +8,9 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
@@ -89,7 +89,7 @@ func (o *reaction) Patch(ctx context.Context) {
}
func (o *reaction) Put(ctx context.Context) f3_id.NodeID {
- o.Error("%v", o.forgejoReaction.User)
+ o.Trace("%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.Error("%v", o.forgejoReaction)
+ o.Trace("%v", o.forgejoReaction)
if _, err := sess.Insert(o.forgejoReaction); err != nil {
panic(err)
diff --git a/services/f3/driver/reactions.go b/services/f3/driver/reactions.go
index b7fd5e8f0a..a546927b92 100644
--- a/services/f3/driver/reactions.go
+++ b/services/f3/driver/reactions.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/release.go b/services/f3/driver/release.go
index 86490e8b02..df38bd8bc0 100644
--- a/services/f3/driver/release.go
+++ b/services/f3/driver/release.go
@@ -9,12 +9,12 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/timeutil"
- release_service "code.gitea.io/gitea/services/release"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/timeutil"
+ release_service "forgejo.org/services/release"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/releases.go b/services/f3/driver/releases.go
index 3b46bc7c54..a631c0b60e 100644
--- a/services/f3/driver/releases.go
+++ b/services/f3/driver/releases.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/repository.go b/services/f3/driver/repository.go
index 118d5f2f2a..3cd9aa7f2e 100644
--- a/services/f3/driver/repository.go
+++ b/services/f3/driver/repository.go
@@ -7,7 +7,7 @@ package driver
import (
"context"
- repo_model "code.gitea.io/gitea/models/repo"
+ repo_model "forgejo.org/models/repo"
"code.forgejo.org/f3/gof3/v3/f3"
helpers_repository "code.forgejo.org/f3/gof3/v3/forges/helpers/repository"
@@ -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 string, internalRefs []string)) {
+func (o *repository) SetFetchFunc(fetchFunc func(ctx context.Context, destination, internalRef string)) {
o.f.FetchFunc = fetchFunc
}
@@ -93,10 +93,16 @@ func (o *repository) GetRepositoryPushURL() string {
return o.getURL()
}
-func (o *repository) GetRepositoryInternalRefs() []string {
- return []string{}
+func (o *repository) GetRepositoryInternalRef() string {
+ return ""
}
+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/review.go b/services/f3/driver/review.go
index d180ea96be..f4f5ff44b8 100644
--- a/services/f3/driver/review.go
+++ b/services/f3/driver/review.go
@@ -8,10 +8,10 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/timeutil"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/reviewcomment.go b/services/f3/driver/reviewcomment.go
index 7ba0e15802..22759b6df3 100644
--- a/services/f3/driver/reviewcomment.go
+++ b/services/f3/driver/reviewcomment.go
@@ -9,10 +9,10 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/timeutil"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/reviewcomments.go b/services/f3/driver/reviewcomments.go
index e11aaa489b..2aa4dea22c 100644
--- a/services/f3/driver/reviewcomments.go
+++ b/services/f3/driver/reviewcomments.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/reviews.go b/services/f3/driver/reviews.go
index a20d5741d1..7c3dcb37de 100644
--- a/services/f3/driver/reviews.go
+++ b/services/f3/driver/reviews.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/tests/init.go b/services/f3/driver/tests/init.go
index d7bf23ac88..9035296dc0 100644
--- a/services/f3/driver/tests/init.go
+++ b/services/f3/driver/tests/init.go
@@ -5,7 +5,7 @@
package tests
import (
- driver_options "code.gitea.io/gitea/services/f3/driver/options"
+ driver_options "forgejo.org/services/f3/driver/options"
tests_forge "code.forgejo.org/f3/gof3/v3/tree/tests/f3/forge"
)
diff --git a/services/f3/driver/tests/new.go b/services/f3/driver/tests/new.go
index dc6ac437e6..2f5c6c64db 100644
--- a/services/f3/driver/tests/new.go
+++ b/services/f3/driver/tests/new.go
@@ -7,7 +7,7 @@ package tests
import (
"testing"
- driver_options "code.gitea.io/gitea/services/f3/driver/options"
+ driver_options "forgejo.org/services/f3/driver/options"
f3_kind "code.forgejo.org/f3/gof3/v3/kind"
"code.forgejo.org/f3/gof3/v3/options"
diff --git a/services/f3/driver/tests/options.go b/services/f3/driver/tests/options.go
index adaa1da588..f61b10c9ef 100644
--- a/services/f3/driver/tests/options.go
+++ b/services/f3/driver/tests/options.go
@@ -7,9 +7,9 @@ package tests
import (
"testing"
- forgejo_log "code.gitea.io/gitea/modules/log"
- driver_options "code.gitea.io/gitea/services/f3/driver/options"
- "code.gitea.io/gitea/services/f3/util"
+ forgejo_log "forgejo.org/modules/log"
+ driver_options "forgejo.org/services/f3/driver/options"
+ "forgejo.org/services/f3/util"
"code.forgejo.org/f3/gof3/v3/options"
)
diff --git a/services/f3/driver/topic.go b/services/f3/driver/topic.go
index eeb387cf93..cc94aa35fa 100644
--- a/services/f3/driver/topic.go
+++ b/services/f3/driver/topic.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/topics.go b/services/f3/driver/topics.go
index 2685a47928..38f03dbd2d 100644
--- a/services/f3/driver/topics.go
+++ b/services/f3/driver/topics.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
"code.forgejo.org/f3/gof3/v3/tree/generic"
diff --git a/services/f3/driver/tree.go b/services/f3/driver/tree.go
index 2377d3794d..fe11b15f6e 100644
--- a/services/f3/driver/tree.go
+++ b/services/f3/driver/tree.go
@@ -8,7 +8,7 @@ import (
"context"
"fmt"
- forgejo_options "code.gitea.io/gitea/services/f3/driver/options"
+ forgejo_options "forgejo.org/services/f3/driver/options"
f3_kind "code.forgejo.org/f3/gof3/v3/kind"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
@@ -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.KindAssets:
- return newAssets()
- case f3_tree.KindAsset:
- return newAsset()
+ case f3_tree.KindAttachments:
+ return newAttachments()
+ case f3_tree.KindAttachment:
+ return newAttachment()
case f3_tree.KindLabels:
return newLabels()
case f3_tree.KindLabel:
diff --git a/services/f3/driver/user.go b/services/f3/driver/user.go
index 0ba6cbb7c6..bf8bfaf9c9 100644
--- a/services/f3/driver/user.go
+++ b/services/f3/driver/user.go
@@ -9,9 +9,9 @@ import (
"fmt"
"strings"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/optional"
- user_service "code.gitea.io/gitea/services/user"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/optional"
+ user_service "forgejo.org/services/user"
"code.forgejo.org/f3/gof3/v3/f3"
f3_id "code.forgejo.org/f3/gof3/v3/id"
diff --git a/services/f3/driver/users.go b/services/f3/driver/users.go
index 59b10fc51d..cb413ae05d 100644
--- a/services/f3/driver/users.go
+++ b/services/f3/driver/users.go
@@ -8,8 +8,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
f3_id "code.forgejo.org/f3/gof3/v3/id"
f3_tree "code.forgejo.org/f3/gof3/v3/tree/f3"
diff --git a/services/f3/util/logger.go b/services/f3/util/logger.go
index 21d8d6bbfa..9a1409ae84 100644
--- a/services/f3/util/logger.go
+++ b/services/f3/util/logger.go
@@ -6,8 +6,8 @@ package util
import (
"fmt"
- forgejo_log "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/migration"
+ forgejo_log "forgejo.org/modules/log"
+ "forgejo.org/modules/migration"
"code.forgejo.org/f3/gof3/v3/logger"
)
diff --git a/services/f3/util/logger_test.go b/services/f3/util/logger_test.go
index db880aa439..f62d9e2e82 100644
--- a/services/f3/util/logger_test.go
+++ b/services/f3/util/logger_test.go
@@ -8,8 +8,8 @@ import (
"testing"
"time"
- forgejo_log "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/test"
+ forgejo_log "forgejo.org/modules/log"
+ "forgejo.org/modules/test"
"code.forgejo.org/f3/gof3/v3/logger"
"github.com/stretchr/testify/assert"
@@ -23,7 +23,7 @@ func TestF3UtilMessage(t *testing.T) {
actual = fmt.Sprintf(message, args...)
}, nil)
logger.Message("EXPECTED %s", "MESSAGE")
- assert.EqualValues(t, expected, actual)
+ assert.Equal(t, expected, actual)
}
func TestF3UtilLogger(t *testing.T) {
diff --git a/services/federation/delivery_queue.go b/services/federation/delivery_queue.go
new file mode 100644
index 0000000000..f71467e9f0
--- /dev/null
+++ b/services/federation/delivery_queue.go
@@ -0,0 +1,76 @@
+// 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
new file mode 100644
index 0000000000..425035d0d5
--- /dev/null
+++ b/services/federation/error.go
@@ -0,0 +1,44 @@
+// 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 4c6f5ca0ca..ccdb9bbab0 100644
--- a/services/federation/federation_service.go
+++ b/services/federation/federation_service.go
@@ -1,151 +1,45 @@
-// Copyright 2024 The Forgejo Authors. All rights reserved.
+// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package federation
import (
"context"
+ "database/sql"
"fmt"
- "net/http"
"net/url"
"strings"
- "time"
- "code.gitea.io/gitea/models/forgefed"
- "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/activitypub"
- "code.gitea.io/gitea/modules/auth/password"
- fm "code.gitea.io/gitea/modules/forgefed"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/validation"
+ "forgejo.org/models/forgefed"
+ "forgejo.org/models/user"
+ "forgejo.org/modules/activitypub"
+ "forgejo.org/modules/auth/password"
+ fm "forgejo.org/modules/forgefed"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/validation"
"github.com/google/uuid"
)
-// 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
+func Init() error {
+ if !setting.Federation.Enabled {
+ return nil
}
- 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
+ return initDeliveryQueue()
}
-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)
+func FindOrCreateFederationHost(ctx context.Context, actorURI string) (*forgefed.FederationHost, error) {
rawActorID, err := fm.NewActorID(actorURI)
if err != nil {
return nil, err
}
- federationHost, err := forgefed.FindFederationHostByFqdn(ctx, rawActorID.Host)
+ federationHost, err := forgefed.FindFederationHostByFqdnAndPort(ctx, rawActorID.Host, rawActorID.HostPort)
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
}
@@ -154,19 +48,109 @@ func GetFederationHostForURI(ctx context.Context, actorURI string) (*forgefed.Fe
return federationHost, nil
}
-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()
+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()
clientFactory, err := activitypub.GetClientFactory(ctx)
if err != nil {
return nil, nil, err
}
- client, err := clientFactory.WithKeys(ctx, actionsUser, "no idea where to get key material.")
+
+ apClient, err := clientFactory.WithKeys(ctx, actionsUser, actionsUser.KeyID())
if err != nil {
return nil, nil, err
}
- body, err := client.GetBody(personID.AsURI())
+ body, err := apClient.GetBody(personID.AsURI())
if err != nil {
return nil, nil, err
}
@@ -176,26 +160,42 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
if err != nil {
return nil, nil, err
}
+
if res, err := validation.IsValid(person); !res {
return nil, nil, err
}
- log.Info("Fetched valid person:%q", person)
+
+ log.Info("Fetched valid person from distant server: %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,89 +207,37 @@ func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostI
LoginName: loginName,
Type: user.UserTypeRemoteUser,
IsAdmin: false,
- NormalizedFederatedURI: personID.AsURI(),
}
- federatedUser := user.FederatedUser{
- ExternalID: personID.ID,
- FederationHostID: federationHostID,
- }
- err = user.CreateFederatedUser(ctx, &newUser, &federatedUser)
- if err != nil {
- return nil, nil, err
- }
- log.Info("Created federatedUser:%q", federatedUser)
+ 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,
+ },
+ }
+
+ log.Info("Fetched person's %q federatedUser from distant server: %q", person, federatedUser)
return &newUser, &federatedUser, nil
}
-// 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)
- }
-
- 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)
+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 err
+ return nil, nil, 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)
+ err = user.CreateFederatedUser(ctx, newUser, federatedUser)
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, nil, err
}
- return nil
+ log.Info("Created federatedUser: %q", federatedUser)
+ return newUser, federatedUser, nil
}
diff --git a/services/federation/person_inbox_accept.go b/services/federation/person_inbox_accept.go
new file mode 100644
index 0000000000..d0a840bd2d
--- /dev/null
+++ b/services/federation/person_inbox_accept.go
@@ -0,0 +1,22 @@
+// 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
new file mode 100644
index 0000000000..2132c7ede1
--- /dev/null
+++ b/services/federation/person_inbox_create.go
@@ -0,0 +1,55 @@
+// 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
new file mode 100644
index 0000000000..baa7934ad5
--- /dev/null
+++ b/services/federation/person_inbox_follow.go
@@ -0,0 +1,73 @@
+// 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
new file mode 100644
index 0000000000..4379cf242a
--- /dev/null
+++ b/services/federation/person_inbox_undo.go
@@ -0,0 +1,47 @@
+// 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
new file mode 100644
index 0000000000..d6482d013c
--- /dev/null
+++ b/services/federation/person_service.go
@@ -0,0 +1,60 @@
+// 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
new file mode 100644
index 0000000000..478a12d92c
--- /dev/null
+++ b/services/federation/repository_inbox_like.go
@@ -0,0 +1,147 @@
+// 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
new file mode 100644
index 0000000000..7891d786e2
--- /dev/null
+++ b/services/federation/repository_service.go
@@ -0,0 +1,19 @@
+// 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
new file mode 100644
index 0000000000..47afb2bdf6
--- /dev/null
+++ b/services/federation/result.go
@@ -0,0 +1,35 @@
+// 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
new file mode 100644
index 0000000000..fd8cbb39cd
--- /dev/null
+++ b/services/federation/signature_service.go
@@ -0,0 +1,235 @@
+// 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
new file mode 100644
index 0000000000..0db2aee4ec
--- /dev/null
+++ b/services/federation/user_activity.go
@@ -0,0 +1,83 @@
+// 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 2d6a6cb09a..96de080691 100644
--- a/services/feed/action.go
+++ b/services/feed/action.go
@@ -9,17 +9,18 @@ import (
"path"
"strings"
- activities_model "code.gitea.io/gitea/models/activities"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- notify_service "code.gitea.io/gitea/services/notify"
+ activities_model "forgejo.org/models/activities"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ federation_service "forgejo.org/services/federation"
+ notify_service "forgejo.org/services/notify"
)
type actionNotifier struct {
@@ -39,6 +40,22 @@ 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)
@@ -50,11 +67,11 @@ func (a *actionNotifier) NewIssue(ctx context.Context, issue *issues_model.Issue
}
repo := issue.Repo
- if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: issue.Poster.ID,
ActUser: issue.Poster,
OpType: activities_model.ActionCreateIssue,
- Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
+ Content: encodeContent(fmt.Sprintf("%d", issue.Index), issue.Title),
RepoID: repo.ID,
Repo: repo,
IsPrivate: repo.IsPrivate,
@@ -70,7 +87,7 @@ func (a *actionNotifier) IssueChangeStatus(ctx context.Context, doer *user_model
act := &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
- Content: fmt.Sprintf("%d|%s", issue.Index, ""),
+ Content: encodeContent(fmt.Sprintf("%d", issue.Index), ""),
RepoID: issue.Repo.ID,
Repo: issue.Repo,
Comment: actionComment,
@@ -91,7 +108,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 := activities_model.NotifyWatchers(ctx, act); err != nil {
+ if err := notifyAll(ctx, act); err != nil {
log.Error("NotifyWatchers: %v", err)
}
}
@@ -108,18 +125,9 @@ 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 {
@@ -127,7 +135,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 := activities_model.NotifyWatchers(ctx, act); err != nil {
+ if err := notifyAll(ctx, act); err != nil {
log.Error("NotifyWatchers: %v", err)
}
}
@@ -146,11 +154,11 @@ func (a *actionNotifier) NewPullRequest(ctx context.Context, pull *issues_model.
return
}
- if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: pull.Issue.Poster.ID,
ActUser: pull.Issue.Poster,
OpType: activities_model.ActionCreatePullRequest,
- Content: fmt.Sprintf("%d|%s", pull.Issue.Index, pull.Issue.Title),
+ Content: encodeContent(fmt.Sprintf("%d", pull.Issue.Index), pull.Issue.Title),
RepoID: pull.Issue.Repo.ID,
Repo: pull.Issue.Repo,
IsPrivate: pull.Issue.Repo.IsPrivate,
@@ -160,7 +168,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionRenameRepo,
@@ -174,7 +182,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionTransferRepo,
@@ -188,7 +196,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionCreateRepo,
@@ -201,7 +209,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionCreateRepo,
@@ -230,7 +238,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: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comm.Content, "\n")[0]),
+ Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), abbreviatedComment(comm.Content)),
OpType: activities_model.ActionCommentPull,
RepoID: review.Issue.RepoID,
Repo: review.Issue.Repo,
@@ -246,7 +254,7 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model
action := &activities_model.Action{
ActUserID: review.Reviewer.ID,
ActUser: review.Reviewer,
- Content: fmt.Sprintf("%d|%s", review.Issue.Index, strings.Split(comment.Content, "\n")[0]),
+ Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), abbreviatedComment(comment.Content)),
RepoID: review.Issue.RepoID,
Repo: review.Issue.Repo,
IsPrivate: review.Issue.Repo.IsPrivate,
@@ -266,17 +274,17 @@ func (a *actionNotifier) PullRequestReview(ctx context.Context, pr *issues_model
actions = append(actions, action)
}
- if err := activities_model.NotifyWatchersActions(ctx, actions); err != nil {
+ if err := notifyAllActions(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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionMergePullRequest,
- Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
+ Content: encodeContent(fmt.Sprintf("%d", pr.Issue.Index), pr.Issue.Title),
RepoID: pr.Issue.Repo.ID,
Repo: pr.Issue.Repo,
IsPrivate: pr.Issue.Repo.IsPrivate,
@@ -286,11 +294,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionAutoMergePullRequest,
- Content: fmt.Sprintf("%d|%s", pr.Issue.Index, pr.Issue.Title),
+ Content: encodeContent(fmt.Sprintf("%d", pr.Issue.Index), pr.Issue.Title),
RepoID: pr.Issue.Repo.ID,
Repo: pr.Issue.Repo,
IsPrivate: pr.Issue.Repo.IsPrivate,
@@ -299,16 +307,16 @@ func (*actionNotifier) AutoMergePullRequest(ctx context.Context, doer *user_mode
}
}
-func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) {
+func (*actionNotifier) PullReviewDismiss(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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: activities_model.ActionPullReviewDismissed,
- Content: fmt.Sprintf("%d|%s|%s", review.Issue.Index, reviewerName, comment.Content),
+ Content: encodeContent(fmt.Sprintf("%d", review.Issue.Index), reviewerName, abbreviatedComment(comment.Content)),
RepoID: review.Issue.Repo.ID,
Repo: review.Issue.Repo,
IsPrivate: review.Issue.Repo.IsPrivate,
@@ -320,9 +328,7 @@ func (*actionNotifier) NotifyPullRevieweDismiss(ctx context.Context, doer *user_
}
func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
- if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
- commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
- }
+ commits = prepareCommitsForFeed(commits)
data, err := json.Marshal(commits)
if err != nil {
@@ -342,7 +348,7 @@ func (a *actionNotifier) PushCommits(ctx context.Context, pusher *user_model.Use
opType = activities_model.ActionDeleteBranch
}
- if err = activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err = notifyAll(ctx, &activities_model.Action{
ActUserID: pusher.ID,
ActUser: pusher,
OpType: opType,
@@ -362,7 +368,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: opType,
@@ -381,7 +387,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: doer.ID,
ActUser: doer,
OpType: opType,
@@ -395,9 +401,7 @@ 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) {
- if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
- commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
- }
+ commits = prepareCommitsForFeed(commits)
data, err := json.Marshal(commits)
if err != nil {
@@ -405,7 +409,7 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model
return
}
- if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: repo.OwnerID,
ActUser: repo.MustOwner(ctx),
OpType: activities_model.ActionMirrorSyncPush,
@@ -420,7 +424,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: repo.OwnerID,
ActUser: repo.MustOwner(ctx),
OpType: activities_model.ActionMirrorSyncCreate,
@@ -434,7 +438,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 := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: repo.OwnerID,
ActUser: repo.MustOwner(ctx),
OpType: activities_model.ActionMirrorSyncDelete,
@@ -452,7 +456,7 @@ func (a *actionNotifier) NewRelease(ctx context.Context, rel *repo_model.Release
log.Error("LoadAttributes: %v", err)
return
}
- if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{
+ if err := notifyAll(ctx, &activities_model.Action{
ActUserID: rel.PublisherID,
ActUser: rel.Publisher,
OpType: activities_model.ActionPublishRelease,
@@ -465,3 +469,73 @@ 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 037cf08dfe..fd27bf32a9 100644
--- a/services/feed/action_test.go
+++ b/services/feed/action_test.go
@@ -1,4 +1,5 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package feed
@@ -7,18 +8,18 @@ import (
"strings"
"testing"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -66,7 +67,7 @@ func pushCommits() *repository.PushCommits {
CommitterName: "User2",
AuthorEmail: "user2@example.com",
AuthorName: "User2",
- Message: "not signed commit",
+ Message: "not signed commit\nline two",
},
{
Sha1: "27566bd",
@@ -82,10 +83,10 @@ func pushCommits() *repository.PushCommits {
CommitterName: "User2",
AuthorEmail: "user2@example.com",
AuthorName: "User2",
- Message: "good signed commit",
+ Message: "good signed commit\nlong commit message\nwith lots of details\nabout how cool the implementation is",
},
}
- pushCommits.HeadCommit = &repository.PushCommit{Sha1: "69554a6"}
+ pushCommits.HeadCommit = &repository.PushCommit{Sha1: "69554a6", Message: "not signed commit\nline two"}
return pushCommits
}
@@ -102,7 +103,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","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)
+ 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)
})
t.Run("Only one commit", func(t *testing.T) {
@@ -112,7 +113,23 @@ 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","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)
+ 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)
})
}
@@ -129,7 +146,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","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)
+ 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)
})
t.Run("Only one commit", func(t *testing.T) {
@@ -139,6 +156,83 @@ 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","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)
+ 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)
})
}
+
+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/forgejo/main_test.go b/services/forgejo/main_test.go
index 40ce1715b1..5523ed1aab 100644
--- a/services/forgejo/main_test.go
+++ b/services/forgejo/main_test.go
@@ -5,12 +5,12 @@ package forgejo
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
- _ "code.gitea.io/gitea/models"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/activities"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/activities"
+ _ "forgejo.org/models/forgefed"
)
func TestMain(m *testing.M) {
diff --git a/services/forgejo/sanity.go b/services/forgejo/sanity.go
index 5e817d67f5..70f15889d4 100644
--- a/services/forgejo/sanity.go
+++ b/services/forgejo/sanity.go
@@ -3,9 +3,9 @@
package forgejo
import (
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/db"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
var (
diff --git a/services/forgejo/sanity_test.go b/services/forgejo/sanity_test.go
index 657f7e2720..065a9fda4d 100644
--- a/services/forgejo/sanity_test.go
+++ b/services/forgejo/sanity_test.go
@@ -7,9 +7,9 @@ import (
"path/filepath"
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/require"
)
diff --git a/services/forgejo/sanity_v1TOv5_0_1Included.go b/services/forgejo/sanity_v1TOv5_0_1Included.go
index 49de636f33..1d3f07d8e1 100644
--- a/services/forgejo/sanity_v1TOv5_0_1Included.go
+++ b/services/forgejo/sanity_v1TOv5_0_1Included.go
@@ -6,9 +6,9 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/forgejo/semver"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/db"
+ "forgejo.org/models/forgejo/semver"
+ "forgejo.org/modules/setting"
"github.com/hashicorp/go-version"
)
diff --git a/services/forgejo/sanity_v1TOv5_0_1Included_test.go b/services/forgejo/sanity_v1TOv5_0_1Included_test.go
index 56618ebd5f..2521afb496 100644
--- a/services/forgejo/sanity_v1TOv5_0_1Included_test.go
+++ b/services/forgejo/sanity_v1TOv5_0_1Included_test.go
@@ -6,10 +6,10 @@ import (
"fmt"
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/forgejo/semver"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/models/db"
+ "forgejo.org/models/forgejo/semver"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/log"
"github.com/stretchr/testify/require"
)
diff --git a/services/forms/admin.go b/services/forms/admin.go
index 1f055cff55..5a5d46634b 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -6,9 +6,9 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index 21443ff6a5..b89e87f749 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
@@ -79,6 +79,7 @@ type AuthenticationForm struct {
SkipLocalTwoFA bool
GroupTeamMap string `binding:"ValidGroupTeamMap"`
GroupTeamMapRemoval bool
+ AllowUsernameChange bool
}
// Validate validates fields
diff --git a/services/forms/org.go b/services/forms/org.go
index dea2e159e9..a6e4e72c4a 100644
--- a/services/forms/org.go
+++ b/services/forms/org.go
@@ -7,9 +7,9 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index 7a7d8752cf..82e5a09f86 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
diff --git a/services/forms/repo_branch_form.go b/services/forms/repo_branch_form.go
index 186a4ad367..c34e7c6d17 100644
--- a/services/forms/repo_branch_form.go
+++ b/services/forms/repo_branch_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 8253a8957b..11aac1fd52 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -12,14 +12,14 @@ import (
"regexp"
"strings"
- "code.gitea.io/gitea/models"
- issues_model "code.gitea.io/gitea/models/issues"
- project_model "code.gitea.io/gitea/models/project"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/models"
+ issues_model "forgejo.org/models/issues"
+ project_model "forgejo.org/models/project"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
@@ -105,6 +105,9 @@ 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)
}
@@ -141,6 +144,7 @@ type RepoSettingForm struct {
PushMirrorSyncOnCommit bool
PushMirrorInterval string
PushMirrorUseSSH bool
+ PushMirrorBranchFilter string `binding:"MaxSize(2048)" preprocess:"TrimSpace"`
Private bool
Template bool
EnablePrune bool
@@ -188,8 +192,8 @@ type RepoUnitSettingForm struct {
PullsAllowSquash bool
PullsAllowFastForwardOnly bool
PullsAllowManualMerge bool
- PullsDefaultMergeStyle string
- PullsDefaultUpdateStyle string
+ PullsDefaultMergeStyle string `binding:"In(merge,rebase,rebase-merge,squash,fast-forward-only,manually-merged,rebase-update-only)"`
+ PullsDefaultUpdateStyle string `binding:"In(merge,rebase)"`
EnableAutodetectManualMerge bool
PullsAllowRebaseUpdate bool
DefaultDeleteBranchAfterMerge bool
@@ -277,6 +281,9 @@ type WebhookCoreForm struct {
Wiki bool
Repository bool
Package bool
+ ActionFailure bool
+ ActionRecover bool
+ ActionSuccess bool
Active bool
BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string
@@ -725,8 +732,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)"`
- Minutes int `binding:"Range(0,1000)"`
+ Hours int `binding:"Range(0,1000)" locale:"repo.issues.add_time_hours"`
+ Minutes int `binding:"Range(0,1000)" locale:"repo.issues.add_time_minutes"`
}
// Validate validates the fields
diff --git a/services/forms/repo_form_test.go b/services/forms/repo_form_test.go
index 2c5a8e2c0f..4047762096 100644
--- a/services/forms/repo_form_test.go
+++ b/services/forms/repo_form_test.go
@@ -6,7 +6,7 @@ package forms
import (
"testing"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/assert"
)
diff --git a/services/forms/repo_tag_form.go b/services/forms/repo_tag_form.go
index 38f5996db3..1254c84d07 100644
--- a/services/forms/repo_tag_form.go
+++ b/services/forms/repo_tag_form.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
diff --git a/services/forms/report_abuse.go b/services/forms/report_abuse.go
new file mode 100644
index 0000000000..5e9d7dc45f
--- /dev/null
+++ b/services/forms/report_abuse.go
@@ -0,0 +1,28 @@
+// 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/runner.go b/services/forms/runner.go
index f933750858..fcf6c5a694 100644
--- a/services/forms/runner.go
+++ b/services/forms/runner.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index d76e97ceb1..8c95139e2c 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -9,12 +9,12 @@ import (
"net/http"
"strings"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/validation"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
@@ -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() bool {
+func (f *RegisterForm) IsEmailDomainAllowed() (validEmail, ok bool) {
return validation.IsEmailDomainAllowed(f.Email)
}
diff --git a/services/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go
index c5ab703fa1..02d4f873bc 100644
--- a/services/forms/user_form_auth_openid.go
+++ b/services/forms/user_form_auth_openid.go
@@ -6,8 +6,8 @@ package forms
import (
"net/http"
- "code.gitea.io/gitea/modules/web/middleware"
- "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/web/middleware"
+ "forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go
index b9677c1800..74a1aaccb0 100644
--- a/services/forms/user_form_hidden_comments.go
+++ b/services/forms/user_form_hidden_comments.go
@@ -6,9 +6,9 @@ package forms
import (
"math/big"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/services/context"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/log"
+ "forgejo.org/services/context"
)
type hiddenCommentTypeGroupsType map[string][]issues_model.CommentType
diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go
index 66050187c9..4ce63ab552 100644
--- a/services/forms/user_form_test.go
+++ b/services/forms/user_form_test.go
@@ -7,33 +7,26 @@ import (
"strconv"
"testing"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/modules/setting"
+ 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) {
- oldService := setting.Service
- defer func() {
- setting.Service = oldService
- }()
-
- setting.Service.EmailDomainAllowList = nil
+ defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, nil)()
form := RegisterForm{}
- assert.True(t, form.IsEmailDomainAllowed())
+ emailValid, ok := form.IsEmailDomainAllowed()
+ assert.False(t, emailValid)
+ assert.False(t, ok)
}
func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
- oldService := setting.Service
- defer func() {
- setting.Service = oldService
- }()
-
- setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io")}
+ defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io")})()
tt := []struct {
email string
@@ -45,17 +38,13 @@ func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) {
for _, v := range tt {
form := RegisterForm{Email: v.email}
- assert.False(t, form.IsEmailDomainAllowed())
+ _, ok := form.IsEmailDomainAllowed()
+ assert.False(t, ok)
}
}
func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) {
- oldService := setting.Service
- defer func() {
- setting.Service = oldService
- }()
-
- setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")}
+ defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")})()
tt := []struct {
email string
@@ -73,18 +62,13 @@ func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) {
for _, v := range tt {
form := RegisterForm{Email: v.email}
- assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
+ _, ok := form.IsEmailDomainAllowed()
+ assert.Equal(t, v.valid, ok)
}
}
func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
- oldService := setting.Service
- defer func() {
- setting.Service = oldService
- }()
-
- setting.Service.EmailDomainAllowList = nil
- setting.Service.EmailDomainBlockList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")}
+ defer test.MockVariableValue(&setting.Service.EmailDomainBlockList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")})()
tt := []struct {
email string
@@ -92,7 +76,6 @@ 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},
@@ -101,7 +84,8 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
for _, v := range tt {
form := RegisterForm{Email: v.email}
- assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
+ _, ok := form.IsEmailDomainAllowed()
+ assert.Equal(t, v.valid, ok)
}
}
diff --git a/services/gitdiff/csv_test.go b/services/gitdiff/csv_test.go
index 1dbe616374..9bffba33fd 100644
--- a/services/gitdiff/csv_test.go
+++ b/services/gitdiff/csv_test.go
@@ -8,9 +8,9 @@ import (
"strings"
"testing"
- "code.gitea.io/gitea/models/db"
- csv_module "code.gitea.io/gitea/modules/csv"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/db"
+ csv_module "forgejo.org/modules/csv"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index f2070983e6..2f3d289c80 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -17,19 +17,19 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- pull_model "code.gitea.io/gitea/models/pull"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/analyze"
- "code.gitea.io/gitea/modules/charset"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/highlight"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ pull_model "forgejo.org/models/pull"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/analyze"
+ "forgejo.org/modules/charset"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/highlight"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
"github.com/sergi/go-diff/diffmatchpatch"
stdcharset "golang.org/x/net/html/charset"
@@ -85,11 +85,20 @@ type DiffLine struct {
// DiffLineSectionInfo represents diff line section meta data
type DiffLineSectionInfo struct {
- Path string
- LastLeftIdx int
- LastRightIdx int
- LeftIdx int
- RightIdx int
+ 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.
LeftHunkSize int
RightHunkSize int
}
@@ -157,7 +166,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 > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {
+ } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx-1 > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {
return DiffLineExpandUpDown
} else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 {
return DiffLineExpandDown
@@ -418,6 +427,7 @@ func (diffFile *DiffFile) ShouldBeHidden() bool {
return diffFile.IsGenerated || diffFile.IsViewed
}
+//llu:returnsTrKey
func (diffFile *DiffFile) ModeTranslationKey(mode string) string {
switch mode {
case "040000":
@@ -440,11 +450,29 @@ func getCommitFileLineCount(commit *git.Commit, filePath string) int {
if err != nil {
return 0
}
- lineCount, err := blob.GetBlobLineCount()
+ reader, err := blob.DataAsync()
if err != nil {
return 0
}
- return lineCount
+ 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)
+ }
}
// Diff represents a difference between two git trees.
@@ -1060,7 +1088,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
@@ -1088,62 +1116,63 @@ type DiffOptions struct {
FileOnly bool
}
-// GetDiff builds a Diff between two commits of a repository.
+// GetDiffSimple 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 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)
+// 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)
if err != nil {
- return nil, err
+ return nil, nil, fmt.Errorf("unable to get the after commit %q: %w", opts.AfterCommitID, err)
}
cmdCtx, cmdCancel := context.WithCancel(ctx)
defer cmdCancel()
- cmdDiff := git.NewCommand(cmdCtx)
+ cmdDiff := git.NewCommand(cmdCtx).
+ AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
+ AddArguments(opts.WhitespaceBehavior...)
+
objectFormat, err := gitRepo.GetObjectFormat()
if err != nil {
- return nil, err
+ return nil, nil, fmt.Errorf("not able to determine the object format: %w", err)
}
- 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)
+ // 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())
} else {
- actualBeforeCommitID := opts.BeforeCommitID
- if len(actualBeforeCommitID) == 0 {
- parentCommit, err := commit.Parent(0)
+ // If before commit ID is empty, use the first parent of the after commit.
+ if len(opts.BeforeCommitID) == 0 {
+ parentCommit, err := afterCommit.Parent(0)
if err != nil {
- return nil, err
+ return nil, nil, fmt.Errorf("not able to get first parent of %q: %w", afterCommit.ID.String(), err)
}
- actualBeforeCommitID = parentCommit.ID.String()
+ opts.BeforeCommitID = parentCommit.ID.String()
}
- cmdDiff.AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
- AddArguments(opts.WhitespaceBehavior...).
- AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID)
- opts.BeforeCommitID = actualBeforeCommitID
-
- beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID)
- if err != nil {
- return nil, err
- }
+ cmdDiff.AddDynamicArguments(opts.BeforeCommitID)
}
+ // Add the after commit to the diff command.
+ cmdDiff.AddDynamicArguments(opts.AfterCommitID)
+
// 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 != "" && git.CheckGitVersionAtLeast("2.31") == nil {
+ if opts.SkipTo != "" {
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()
@@ -1153,6 +1182,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
}()
go func() {
+ repoPath := gitRepo.Path
stderr := &bytes.Buffer{}
cmdDiff.SetDescription(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath))
if err := cmdDiff.Run(&git.RunOpts{
@@ -1167,14 +1197,33 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
_ = 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, fmt.Errorf("unable to ParsePatch: %w", err)
+ return nil, nil, fmt.Errorf("unable to parse a git diff: %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)
@@ -1210,7 +1259,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
diffFile.IsGenerated = analyze.IsGenerated(diffFile.Name)
}
- tailSection := diffFile.GetTailSection(gitRepo, beforeCommit, commit)
+ tailSection := diffFile.GetTailSection(gitRepo, beforeCommit, afterCommit)
if tailSection != nil {
diffFile.Sections = append(diffFile.Sections, tailSection)
}
@@ -1272,7 +1321,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 := GetDiff(ctx, gitRepo, opts, files...)
+ diff, err := GetDiffFull(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 f2c099d554..d4d1cd4460 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -9,13 +9,13 @@ import (
"strings"
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/setting"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
@@ -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 := GetDiff(db.DefaultContext, gitRepo,
+ diffs, _, err := GetDiffSimple(db.DefaultContext, gitRepo,
&DiffOptions{
AfterCommitID: "bd7063cc7c04689c4d082183d32a604ed27a24f9",
BeforeCommitID: "559c156f8e0178b71cb44355428f24001b08fc68",
@@ -651,6 +651,229 @@ 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 c72959ea16..61d52d91e6 100644
--- a/services/gitdiff/highlightdiff.go
+++ b/services/gitdiff/highlightdiff.go
@@ -6,7 +6,7 @@ package gitdiff
import (
"strings"
- "code.gitea.io/gitea/modules/highlight"
+ "forgejo.org/modules/highlight"
"github.com/sergi/go-diff/diffmatchpatch"
)
@@ -14,13 +14,14 @@ 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++ {
- if s[pos1] == '<' {
+ switch s[pos1] {
+ case '<':
pos2 := strings.IndexByte(s[pos1:], '>')
if pos2 == -1 {
return "", "", s, false
}
return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true
- } else if s[pos1] == '&' {
+ case '&':
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 2ff4472bcc..0070173b9f 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.Equal(t, "", diff.Text)
+ assert.Empty(t, diff.Text)
}
func TestDiffWithHighlightPlaceholder(t *testing.T) {
@@ -53,8 +53,8 @@ func TestDiffWithHighlightPlaceholder(t *testing.T) {
"a='\U00100000'",
"a='\U0010FFFD''",
)
- assert.Equal(t, "", hcd.PlaceholderTokenMap[0x00100000])
- assert.Equal(t, "", hcd.PlaceholderTokenMap[0x0010FFFD])
+ assert.Empty(t, hcd.PlaceholderTokenMap[0x00100000])
+ assert.Empty(t, hcd.PlaceholderTokenMap[0x0010FFFD])
expected := fmt.Sprintf(`a = ' %s '`, "\U00100000")
output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel)
diff --git a/services/gitdiff/main_test.go b/services/gitdiff/main_test.go
index 3d4d480530..cd7a6a4a6b 100644
--- a/services/gitdiff/main_test.go
+++ b/services/gitdiff/main_test.go
@@ -6,12 +6,12 @@ package gitdiff
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
- _ "code.gitea.io/gitea/models"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/activities"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/activities"
+ _ "forgejo.org/models/forgefed"
)
func TestMain(m *testing.M) {
diff --git a/services/indexer/indexer.go b/services/indexer/indexer.go
index 38dd012a51..92036f95c3 100644
--- a/services/indexer/indexer.go
+++ b/services/indexer/indexer.go
@@ -4,10 +4,10 @@
package indexer
import (
- code_indexer "code.gitea.io/gitea/modules/indexer/code"
- issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
- stats_indexer "code.gitea.io/gitea/modules/indexer/stats"
- notify_service "code.gitea.io/gitea/services/notify"
+ code_indexer "forgejo.org/modules/indexer/code"
+ issue_indexer "forgejo.org/modules/indexer/issues"
+ stats_indexer "forgejo.org/modules/indexer/stats"
+ notify_service "forgejo.org/services/notify"
)
// Init initialize the repo indexer
diff --git a/services/indexer/notify.go b/services/indexer/notify.go
index e2cfe477d3..ddd89f733c 100644
--- a/services/indexer/notify.go
+++ b/services/indexer/notify.go
@@ -6,16 +6,16 @@ package indexer
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- code_indexer "code.gitea.io/gitea/modules/indexer/code"
- issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
- stats_indexer "code.gitea.io/gitea/modules/indexer/stats"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- notify_service "code.gitea.io/gitea/services/notify"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ code_indexer "forgejo.org/modules/indexer/code"
+ issue_indexer "forgejo.org/modules/indexer/issues"
+ stats_indexer "forgejo.org/modules/indexer/stats"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ notify_service "forgejo.org/services/notify"
)
type indexerNotifier struct {
diff --git a/services/issue/assignee.go b/services/issue/assignee.go
index 3d6d0b881a..a5f9c2731f 100644
--- a/services/issue/assignee.go
+++ b/services/issue/assignee.go
@@ -6,15 +6,15 @@ package issue
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- notify_service "code.gitea.io/gitea/services/notify"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ notify_service "forgejo.org/services/notify"
)
// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
diff --git a/services/issue/assignee_test.go b/services/issue/assignee_test.go
index 2b70b8c8ce..66a66459cb 100644
--- a/services/issue/assignee_test.go
+++ b/services/issue/assignee_test.go
@@ -6,10 +6,10 @@ package issue
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/issue/comments.go b/services/issue/comments.go
index 3ab577b83f..2cac900d41 100644
--- a/services/issue/comments.go
+++ b/services/issue/comments.go
@@ -5,20 +5,21 @@ package issue
import (
"context"
+ "errors"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/timeutil"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/timeutil"
+ notify_service "forgejo.org/services/notify"
)
// 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 fmt.Errorf("cannot create reference with empty commit SHA")
+ return errors.New("cannot create reference with empty commit SHA")
}
// Check if same reference from same commit has already existed.
@@ -119,7 +120,28 @@ 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 {
- return issues_model.DeleteComment(ctx, comment)
+ 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
})
if err != nil {
return err
diff --git a/services/issue/comments_test.go b/services/issue/comments_test.go
index 62547a584a..fcf06d9ec8 100644
--- a/services/issue/comments_test.go
+++ b/services/issue/comments_test.go
@@ -6,17 +6,19 @@ package issue_test
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
- issue_service "code.gitea.io/gitea/services/issue"
- "code.gitea.io/gitea/tests"
+ "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"
+ "forgejo.org/tests"
- _ "code.gitea.io/gitea/services/webhook"
+ _ "forgejo.org/services/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -48,9 +50,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.EqualValues(t, issue.NumComments-1, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).NumComments)
+ assert.Equal(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.EqualValues(t, hookTaskCount+1, unittest.GetCount(t, &webhook_model.HookTask{}))
+ assert.Equal(t, hookTaskCount+1, unittest.GetCount(t, &webhook_model.HookTask{}))
})
t.Run("Comment of pending review", func(t *testing.T) {
@@ -59,7 +61,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.EqualValues(t, issues_model.ReviewTypePending, review.Type)
+ assert.Equal(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{
@@ -69,14 +71,17 @@ 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.EqualValues(t, issue.NumComments, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).NumComments)
+ assert.Equal(t, issue.NumComments, unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID}).NumComments)
// No notification was fired for the deletion of this comment.
- assert.EqualValues(t, hookTaskCount, unittest.GetCount(t, &webhook_model.HookTask{}))
+ 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})
})
}
@@ -105,11 +110,11 @@ func TestUpdateComment(t *testing.T) {
newComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
// Content was updated.
- assert.EqualValues(t, comment.Content, newComment.Content)
+ assert.Equal(t, comment.Content, newComment.Content)
// Content version was updated.
- assert.EqualValues(t, 2, newComment.ContentVersion)
+ assert.Equal(t, 2, newComment.ContentVersion)
// A notification was fired for the update of this comment.
- assert.EqualValues(t, hookTaskCount+1, unittest.GetCount(t, &webhook_model.HookTask{}))
+ assert.Equal(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")
@@ -120,7 +125,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.EqualValues(t, issues_model.ReviewTypePending, review.Type)
+ assert.Equal(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{
@@ -136,12 +141,49 @@ func TestUpdateComment(t *testing.T) {
newComment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
// Content was updated.
- assert.EqualValues(t, comment.Content, newComment.Content)
+ assert.Equal(t, comment.Content, newComment.Content)
// Content version was updated.
- assert.EqualValues(t, 2, newComment.ContentVersion)
+ assert.Equal(t, 2, newComment.ContentVersion)
// No notification was fired for the update of this comment.
- assert.EqualValues(t, hookTaskCount, unittest.GetCount(t, &webhook_model.HookTask{}))
+ assert.Equal(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/commit.go b/services/issue/commit.go
index 8b927d52b6..1e51fb32b7 100644
--- a/services/issue/commit.go
+++ b/services/issue/commit.go
@@ -13,15 +13,15 @@ import (
"strings"
"time"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/references"
- "code.gitea.io/gitea/modules/repository"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/references"
+ "forgejo.org/modules/repository"
)
const (
diff --git a/services/issue/commit_test.go b/services/issue/commit_test.go
index c3c3e4c042..e3a41d2305 100644
--- a/services/issue/commit_test.go
+++ b/services/issue/commit_test.go
@@ -6,14 +6,14 @@ package issue
import (
"testing"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
+ activities_model "forgejo.org/models/activities"
+ "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"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/require"
)
diff --git a/services/issue/content.go b/services/issue/content.go
index 612a9a6b4c..d5c79e5fde 100644
--- a/services/issue/content.go
+++ b/services/issue/content.go
@@ -6,9 +6,9 @@ package issue
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- notify_service "code.gitea.io/gitea/services/notify"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ notify_service "forgejo.org/services/notify"
)
// ChangeContent changes issue content, as the given user.
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 5e726176d0..7071a912b0 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -1,26 +1,28 @@
// 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"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- project_model "code.gitea.io/gitea/models/project"
- repo_model "code.gitea.io/gitea/models/repo"
- system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/timeutil"
- notify_service "code.gitea.io/gitea/services/notify"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ project_model "forgejo.org/models/project"
+ repo_model "forgejo.org/models/repo"
+ system_model "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/timeutil"
+ notify_service "forgejo.org/services/notify"
)
// NewIssue creates new issue with labels for repository.
@@ -59,7 +61,6 @@ 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
@@ -73,6 +74,12 @@ 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
}
@@ -252,6 +259,12 @@ 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
}
@@ -333,13 +346,13 @@ func SetIssueUpdateDate(ctx context.Context, issue *issues_model.Issue, updated
return err
}
if !perm.IsAdmin() && !perm.IsOwner() {
- return fmt.Errorf("user needs to have admin or owner right")
+ return errors.New("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 fmt.Errorf("unallowed update date")
+ return errors.New("unallowed update date")
}
issue.UpdatedUnix = updatedUnix
diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go
index a0bb88e387..fb2b2870bd 100644
--- a/services/issue/issue_test.go
+++ b/services/issue/issue_test.go
@@ -6,11 +6,11 @@ package issue
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ "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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -25,8 +25,8 @@ func TestGetRefEndNamesAndURLs(t *testing.T) {
repoLink := "/foo/bar"
endNames, urls := GetRefEndNamesAndURLs(issues, repoLink)
- assert.EqualValues(t, map[int64]string{1: "branch1", 2: "tag1", 3: "c0ffee"}, endNames)
- assert.EqualValues(t, map[int64]string{
+ assert.Equal(t, map[int64]string{1: "branch1", 2: "tag1", 3: "c0ffee"}, endNames)
+ assert.Equal(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 6b8070d8aa..f18dd67a19 100644
--- a/services/issue/label.go
+++ b/services/issue/label.go
@@ -6,11 +6,10 @@ package issue
import (
"context"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- user_model "code.gitea.io/gitea/models/user"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ notify_service "forgejo.org/services/notify"
)
// ClearLabels clears all of an issue's labels
@@ -56,17 +55,6 @@ 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/label_test.go b/services/issue/label_test.go
index b9d26345c1..73a028684b 100644
--- a/services/issue/label_test.go
+++ b/services/issue/label_test.go
@@ -6,10 +6,10 @@ package issue
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
"github.com/stretchr/testify/require"
)
diff --git a/services/issue/main_test.go b/services/issue/main_test.go
index c3da441537..673ec5e4cc 100644
--- a/services/issue/main_test.go
+++ b/services/issue/main_test.go
@@ -6,11 +6,11 @@ package issue
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/webhook"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/webhook"
- _ "code.gitea.io/gitea/models/actions"
+ _ "forgejo.org/models/actions"
)
func TestMain(m *testing.M) {
diff --git a/services/issue/milestone.go b/services/issue/milestone.go
index 407ad0a59b..a561bf8eee 100644
--- a/services/issue/milestone.go
+++ b/services/issue/milestone.go
@@ -5,12 +5,13 @@ package issue
import (
"context"
+ "errors"
"fmt"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ notify_service "forgejo.org/services/notify"
)
func updateMilestoneCounters(ctx context.Context, issue *issues_model.Issue, id int64) error {
@@ -47,7 +48,7 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is
return fmt.Errorf("HasMilestoneByRepoID: %w", err)
}
if !has {
- return fmt.Errorf("HasMilestoneByRepoID: issue doesn't exist")
+ return errors.New("HasMilestoneByRepoID: issue doesn't exist")
}
}
diff --git a/services/issue/milestone_test.go b/services/issue/milestone_test.go
index e75f64550c..4123433c2a 100644
--- a/services/issue/milestone_test.go
+++ b/services/issue/milestone_test.go
@@ -6,10 +6,10 @@ package issue
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/issue/pull.go b/services/issue/pull.go
index 3b61c00afa..6245344ccb 100644
--- a/services/issue/pull.go
+++ b/services/issue/pull.go
@@ -8,15 +8,15 @@ import (
"fmt"
"time"
- issues_model "code.gitea.io/gitea/models/issues"
- org_model "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ issues_model "forgejo.org/models/issues"
+ org_model "forgejo.org/models/organization"
+ access_model "forgejo.org/models/perm/access"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
func getMergeBase(repo *git.Repository, pr *issues_model.PullRequest, baseBranch, headBranch string) (string, error) {
@@ -43,8 +43,6 @@ 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
}
@@ -72,18 +70,17 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
return nil, err
}
- var data string
- for _, file := range files {
+ var rules []*issues_model.CodeOwnerRule
+ for _, file := range []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS", ".forgejo/CODEOWNERS"} {
if blob, err := commit.GetBlobByPath(file); err == nil {
- data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
+ rc, size, err := blob.NewTruncatedReader(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/reaction.go b/services/issue/reaction.go
index dbb4735de2..c6a11aa0f0 100644
--- a/services/issue/reaction.go
+++ b/services/issue/reaction.go
@@ -5,8 +5,8 @@ package issue
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
)
// CreateIssueReaction creates a reaction on issue.
diff --git a/services/issue/status.go b/services/issue/status.go
index 9b6c683f4f..6664da7daa 100644
--- a/services/issue/status.go
+++ b/services/issue/status.go
@@ -6,10 +6,10 @@ package issue
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- notify_service "code.gitea.io/gitea/services/notify"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ notify_service "forgejo.org/services/notify"
)
// ChangeStatus changes issue status to open or closed.
diff --git a/services/issue/template.go b/services/issue/template.go
index 9a2b048401..07f59af630 100644
--- a/services/issue/template.go
+++ b/services/issue/template.go
@@ -10,13 +10,13 @@ import (
"path"
"strings"
- "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/issue/template"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/issue/template"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
)
// templateDirCandidates issue templates directory
@@ -31,6 +31,8 @@ var templateDirCandidates = []string{
".github/issue_template",
".gitlab/ISSUE_TEMPLATE",
".gitlab/issue_template",
+ "docs/ISSUE_TEMPLATE",
+ "docs/issue_template",
}
var templateConfigCandidates = []string{
@@ -40,6 +42,8 @@ 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/locks.go b/services/lfs/locks.go
index 2a362b1c0d..a45b2cc93b 100644
--- a/services/lfs/locks.go
+++ b/services/lfs/locks.go
@@ -8,16 +8,16 @@ import (
"strconv"
"strings"
- auth_model "code.gitea.io/gitea/models/auth"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/json"
- lfs_module "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
+ auth_model "forgejo.org/models/auth"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/json"
+ lfs_module "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/services/context"
+ "forgejo.org/services/convert"
)
func handleLockListOut(ctx *context.Context, repo *repo_model.Repository, lock *git_model.LFSLock, err error) {
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 51d6f42776..17e6d0eec7 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -18,21 +18,21 @@ import (
"strconv"
"strings"
- actions_model "code.gitea.io/gitea/models/actions"
- auth_model "code.gitea.io/gitea/models/auth"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- quota_model "code.gitea.io/gitea/models/quota"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/json"
- lfs_module "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/services/context"
+ actions_model "forgejo.org/models/actions"
+ auth_model "forgejo.org/models/auth"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ quota_model "forgejo.org/models/quota"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/json"
+ lfs_module "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/services/context"
"github.com/golang-jwt/jwt/v5"
)
@@ -163,11 +163,12 @@ func BatchHandler(ctx *context.Context) {
}
var isUpload bool
- if br.Operation == "upload" {
+ switch br.Operation {
+ case "upload":
isUpload = true
- } else if br.Operation == "download" {
+ case "download":
isUpload = false
- } else {
+ default:
log.Trace("Attempt to BATCH with invalid operation: %s", br.Operation)
writeStatus(ctx, http.StatusBadRequest)
return
@@ -594,15 +595,15 @@ func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repo
claims, claimsOk := token.Claims.(*Claims)
if !token.Valid || !claimsOk {
- return nil, fmt.Errorf("invalid token claim")
+ return nil, errors.New("invalid token claim")
}
if claims.RepoID != target.ID {
- return nil, fmt.Errorf("invalid token claim")
+ return nil, errors.New("invalid token claim")
}
if mode == perm.AccessModeWrite && claims.Op != "upload" {
- return nil, fmt.Errorf("invalid token claim")
+ return nil, errors.New("invalid token claim")
}
u, err := user_model.GetUserByID(ctx, claims.UserID)
@@ -615,12 +616,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, fmt.Errorf("no token")
+ return nil, errors.New("no token")
}
parts := strings.SplitN(authorization, " ", 2)
if len(parts) != 2 {
- return nil, fmt.Errorf("no token")
+ return nil, errors.New("no token")
}
tokenSHA := parts[1]
switch strings.ToLower(parts[0]) {
@@ -629,7 +630,7 @@ func parseToken(ctx stdCtx.Context, authorization string, target *repo_model.Rep
case "token":
return handleLFSToken(ctx, tokenSHA, target, mode)
}
- return nil, fmt.Errorf("token not found")
+ return nil, errors.New("token not found")
}
func requireAuth(ctx *context.Context) {
diff --git a/services/mailer/incoming/incoming.go b/services/mailer/incoming/incoming.go
index 1b1be4c656..b1b9191df3 100644
--- a/services/mailer/incoming/incoming.go
+++ b/services/mailer/incoming/incoming.go
@@ -14,10 +14,10 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/mailer/token"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/mailer/token"
"code.forgejo.org/forgejo/reply"
"github.com/emersion/go-imap"
diff --git a/services/mailer/incoming/incoming_handler.go b/services/mailer/incoming/incoming_handler.go
index dc3c4ec69b..7505148978 100644
--- a/services/mailer/incoming/incoming_handler.go
+++ b/services/mailer/incoming/incoming_handler.go
@@ -8,19 +8,19 @@ import (
"context"
"fmt"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- attachment_service "code.gitea.io/gitea/services/attachment"
- "code.gitea.io/gitea/services/context/upload"
- issue_service "code.gitea.io/gitea/services/issue"
- incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
- "code.gitea.io/gitea/services/mailer/token"
- pull_service "code.gitea.io/gitea/services/pull"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ attachment_service "forgejo.org/services/attachment"
+ "forgejo.org/services/context/upload"
+ issue_service "forgejo.org/services/issue"
+ incoming_payload "forgejo.org/services/mailer/incoming/payload"
+ "forgejo.org/services/mailer/token"
+ pull_service "forgejo.org/services/pull"
)
type MailHandler interface {
diff --git a/services/mailer/incoming/payload/payload.go b/services/mailer/incoming/payload/payload.go
index 00ada7826b..bb7a65e3d5 100644
--- a/services/mailer/incoming/payload/payload.go
+++ b/services/mailer/incoming/payload/payload.go
@@ -6,8 +6,8 @@ package payload
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/util"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/util"
)
const replyPayloadVersion1 byte = 1
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index bfede28bbe..410fdf6894 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -16,21 +16,21 @@ import (
texttmpl "text/template"
"time"
- activities_model "code.gitea.io/gitea/models/activities"
- auth_model "code.gitea.io/gitea/models/auth"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/emoji"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/translation"
- incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
- "code.gitea.io/gitea/services/mailer/token"
+ activities_model "forgejo.org/models/activities"
+ auth_model "forgejo.org/models/auth"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/emoji"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/translation"
+ incoming_payload "forgejo.org/services/mailer/incoming/payload"
+ "forgejo.org/services/mailer/token"
"gopkg.in/gomail.v2"
)
@@ -685,19 +685,14 @@ func SendRemovedSecurityKey(ctx context.Context, u *user_model.User, securityKey
}
locale := translation.NewLocale(u.Language)
- hasWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(ctx, u.ID)
- if err != nil {
- return err
- }
- hasTOTP, err := auth_model.HasTwoFactorByUID(ctx, u.ID)
+ hasTwoFactor, err := auth_model.HasTwoFactorByUID(ctx, u.ID)
if err != nil {
return err
}
data := map[string]any{
"locale": locale,
- "HasWebAuthn": hasWebAuthn,
- "HasTOTP": hasTOTP,
+ "HasTwoFactor": hasTwoFactor,
"SecurityKeyName": securityKeyName,
"DisplayName": u.DisplayName(),
"Username": u.Name,
diff --git a/services/mailer/mail_actions.go b/services/mailer/mail_actions.go
new file mode 100644
index 0000000000..1119781613
--- /dev/null
+++ b/services/mailer/mail_actions.go
@@ -0,0 +1,87 @@
+// 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
new file mode 100644
index 0000000000..e84441f460
--- /dev/null
+++ b/services/mailer/mail_actions_now_done_test.go
@@ -0,0 +1,325 @@
+// 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.go b/services/mailer/mail_admin_new_user.go
index 0713de8a95..ffb03197b7 100644
--- a/services/mailer/mail_admin_new_user.go
+++ b/services/mailer/mail_admin_new_user.go
@@ -7,12 +7,12 @@ import (
"context"
"strconv"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/translation"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/templates"
+ "forgejo.org/modules/translation"
)
const (
diff --git a/services/mailer/mail_admin_new_user_test.go b/services/mailer/mail_admin_new_user_test.go
index 765c8cb6c9..58afcfcda6 100644
--- a/services/mailer/mail_admin_new_user_test.go
+++ b/services/mailer/mail_admin_new_user_test.go
@@ -4,20 +4,19 @@
package mailer
import (
- "context"
"strconv"
"testing"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-func getTestUsers(t *testing.T) []*user_model.User {
+func getAdminNewUserTestUsers(t *testing.T) []*user_model.User {
t.Helper()
admin := new(user_model.User)
admin.Name = "testadmin"
@@ -38,16 +37,10 @@ func getTestUsers(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 := getTestUsers(t)
+ users := getAdminNewUserTestUsers(t)
t.Run("SendNotificationEmailOnNewUser_true", func(t *testing.T) {
defer test.MockVariableValue(&setting.Admin.SendNotificationEmailOnNewUser, true)()
@@ -75,5 +68,5 @@ func TestAdminNotificationMail_test(t *testing.T) {
MailNewUser(ctx, users[1])
})
- cleanUpUsers(ctx, users)
+ CleanUpUsers(ctx, users)
}
diff --git a/services/mailer/mail_auth_test.go b/services/mailer/mail_auth_test.go
index 38e3721a22..e40a0d6fa0 100644
--- a/services/mailer/mail_auth_test.go
+++ b/services/mailer/mail_auth_test.go
@@ -6,14 +6,14 @@ package mailer_test
import (
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation"
- "code.gitea.io/gitea/services/mailer"
- user_service "code.gitea.io/gitea/services/user"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
+ "forgejo.org/services/mailer"
+ user_service "forgejo.org/services/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go
index 1812441d5a..b4ed3145ed 100644
--- a/services/mailer/mail_comment.go
+++ b/services/mailer/mail_comment.go
@@ -6,12 +6,12 @@ package mailer
import (
"context"
- activities_model "code.gitea.io/gitea/models/activities"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ activities_model "forgejo.org/models/activities"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
// MailParticipantsComment sends new comment emails to repository watchers and mentioned people.
diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go
index 1bb6fdc7a3..0d8e054041 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -7,15 +7,15 @@ import (
"context"
"fmt"
- activities_model "code.gitea.io/gitea/models/activities"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ activities_model "forgejo.org/models/activities"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
func fallbackMailSubject(issue *issues_model.Issue) string {
@@ -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,9 +137,8 @@ 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_release.go b/services/mailer/mail_release.go
index 0b8b97e9cd..0f2ef33fe1 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -7,14 +7,14 @@ import (
"bytes"
"context"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/markdown"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/markup/markdown"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
)
const (
diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go
index 7003584786..eed650f3ac 100644
--- a/services/mailer/mail_repo.go
+++ b/services/mailer/mail_repo.go
@@ -8,11 +8,11 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation"
+ "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
)
// SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created
diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go
index ceecefa50f..5375133415 100644
--- a/services/mailer/mail_team_invite.go
+++ b/services/mailer/mail_team_invite.go
@@ -6,15 +6,16 @@ package mailer
import (
"bytes"
"context"
+ "errors"
"fmt"
"net/url"
- org_model "code.gitea.io/gitea/models/organization"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/translation"
+ org_model "forgejo.org/models/organization"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/translation"
)
const (
@@ -39,7 +40,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 fmt.Errorf("login is prohibited for the invited user")
+ return errors.New("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 43e5d83890..afbcb8064e 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -15,15 +15,15 @@ import (
"testing"
texttmpl "text/template"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
+ activities_model "forgejo.org/models/activities"
+ "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"
+ "forgejo.org/modules/markup"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -494,8 +494,7 @@ func Test_createReference(t *testing.T) {
func TestFromDisplayName(t *testing.T) {
template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
require.NoError(t, err)
- setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
- defer func() { setting.MailService = nil }()
+ defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromDisplayNameFormatTemplate: template})()
tests := []struct {
userDisplayName string
@@ -518,24 +517,18 @@ 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.EqualValues(t, tc.fromDisplayName, got)
+ assert.Equal(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)
- 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
- }()
+ defer test.MockVariableValue(&setting.MailService, &setting.Mailer{FromDisplayNameFormatTemplate: template})()
+ defer test.MockVariableValue(&setting.AppName, "Code IT")()
+ defer test.MockVariableValue(&setting.Domain, "code.it")()
- assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
+ assert.Equal(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 0a723f974a..d8646d9ddd 100644
--- a/services/mailer/mailer.go
+++ b/services/mailer/mailer.go
@@ -8,6 +8,7 @@ import (
"bytes"
"context"
"crypto/tls"
+ "errors"
"fmt"
"hash/fnv"
"io"
@@ -18,17 +19,17 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/templates"
+ notify_service "forgejo.org/services/notify"
ntlmssp "github.com/Azure/go-ntlmssp"
- "github.com/jaytaylor/html2text"
+ "github.com/inbucket/html2text"
"gopkg.in/gomail.v2"
)
@@ -176,7 +177,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, fmt.Errorf("ntlm ChallengeMessage is empty")
+ return nil, errors.New("ntlm ChallengeMessage is empty")
}
authenticateMessage, err := ntlmssp.ProcessChallenge(fromServer, a.username, a.password, a.domainNeeded)
return authenticateMessage, err
@@ -264,7 +265,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 fmt.Errorf("SMTP server does not support AUTH, but credentials provided")
+ return errors.New("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 045701f3a5..34fd847c05 100644
--- a/services/mailer/mailer_test.go
+++ b/services/mailer/mailer_test.go
@@ -8,9 +8,9 @@ import (
"testing"
"time"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -72,7 +72,7 @@ func TestToMessage(t *testing.T) {
_, err := m1.ToMessage().WriteTo(buf)
require.NoError(t, err)
header, _ := extractMailHeaderAndContent(t, buf.String())
- assert.EqualValues(t, map[string]string{
+ assert.Equal(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.EqualValues(t, map[string]string{
+ assert.Equal(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 908976e7ef..5e9cbe3e99 100644
--- a/services/mailer/main_test.go
+++ b/services/mailer/main_test.go
@@ -7,13 +7,16 @@ import (
"context"
"testing"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/translation"
+ "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"
+ "forgejo.org/modules/translation"
- _ "code.gitea.io/gitea/models/actions"
+ _ "forgejo.org/models/actions"
"github.com/stretchr/testify/assert"
)
@@ -46,3 +49,14 @@ 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 54ab80aab9..640de31fcc 100644
--- a/services/mailer/notify.go
+++ b/services/mailer/notify.go
@@ -7,12 +7,13 @@ import (
"context"
"fmt"
- activities_model "code.gitea.io/gitea/models/activities"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- notify_service "code.gitea.io/gitea/services/notify"
+ actions_model "forgejo.org/models/actions"
+ activities_model "forgejo.org/models/activities"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ notify_service "forgejo.org/services/notify"
)
type mailNotifier struct {
@@ -30,15 +31,16 @@ 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
- if comment.Type == issues_model.CommentTypeClose {
+ switch comment.Type {
+ case issues_model.CommentTypeClose:
act = activities_model.ActionCloseIssue
- } else if comment.Type == issues_model.CommentTypeReopen {
+ case issues_model.CommentTypeReopen:
act = activities_model.ActionReopenIssue
- } else if comment.Type == issues_model.CommentTypeComment {
+ case issues_model.CommentTypeComment:
act = activities_model.ActionCommentIssue
- } else if comment.Type == issues_model.CommentTypeCode {
+ case issues_model.CommentTypeCode:
act = activities_model.ActionCommentIssue
- } else if comment.Type == issues_model.CommentTypePullRequestPush {
+ case issues_model.CommentTypePullRequestPush:
act = 0
}
@@ -94,11 +96,12 @@ 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
- if comment.Type == issues_model.CommentTypeClose {
+ switch comment.Type {
+ case issues_model.CommentTypeClose:
act = activities_model.ActionCloseIssue
- } else if comment.Type == issues_model.CommentTypeReopen {
+ case issues_model.CommentTypeReopen:
act = activities_model.ActionReopenIssue
- } else if comment.Type == issues_model.CommentTypeComment {
+ case issues_model.CommentTypeComment:
act = activities_model.ActionCommentPull
}
if err := MailParticipantsComment(ctx, comment, act, pr.Issue, mentions); err != nil {
@@ -206,3 +209,13 @@ 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/mailer/token/token.go b/services/mailer/token/token.go
index 1a52bce803..f3d7286cb0 100644
--- a/services/mailer/token/token.go
+++ b/services/mailer/token/token.go
@@ -11,8 +11,8 @@ import (
"fmt"
"time"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/util"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/util"
)
// A token is a verifiable container describing an action.
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
index 89fe3e7e34..1b085b4929 100644
--- a/services/markup/main_test.go
+++ b/services/markup/main_test.go
@@ -6,7 +6,7 @@ package markup
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 40bf1d65da..2f1b1e738c 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -5,18 +5,18 @@ package markup
import (
"context"
- "fmt"
+ "errors"
- "code.gitea.io/gitea/models/perm/access"
- "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- gitea_context "code.gitea.io/gitea/services/context"
- file_service "code.gitea.io/gitea/services/repository/files"
+ "forgejo.org/models/perm/access"
+ "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ gitea_context "forgejo.org/services/context"
+ file_service "forgejo.org/services/repository/files"
)
func ProcessorHelper() *markup.ProcessorHelper {
@@ -55,7 +55,7 @@ func ProcessorHelper() *markup.ProcessorHelper {
return nil, err
}
if !perms.CanRead(unit.TypeCode) {
- return nil, fmt.Errorf("cannot access repository code")
+ return nil, errors.New("cannot access repository code")
}
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
diff --git a/services/markup/processorhelper_test.go b/services/markup/processorhelper_test.go
index 4d103048b5..8195451746 100644
--- a/services/markup/processorhelper_test.go
+++ b/services/markup/processorhelper_test.go
@@ -8,11 +8,11 @@ import (
"net/http/httptest"
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/models/user"
- gitea_context "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/contexttest"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ "forgejo.org/models/user"
+ gitea_context "forgejo.org/services/context"
+ "forgejo.org/services/contexttest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/migrations/codebase.go b/services/migrations/codebase.go
index 492fc908e9..843df0f973 100644
--- a/services/migrations/codebase.go
+++ b/services/migrations/codebase.go
@@ -13,10 +13,10 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/proxy"
+ "forgejo.org/modules/structs"
)
var (
diff --git a/services/migrations/codebase_test.go b/services/migrations/codebase_test.go
index fbd4e70143..315c7be709 100644
--- a/services/migrations/codebase_test.go
+++ b/services/migrations/codebase_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"time"
- base "code.gitea.io/gitea/modules/migration"
+ base "forgejo.org/modules/migration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/migrations/common.go b/services/migrations/common.go
index d88518899d..ee74461447 100644
--- a/services/migrations/common.go
+++ b/services/migrations/common.go
@@ -7,10 +7,10 @@ import (
"fmt"
"strings"
- system_model "code.gitea.io/gitea/models/system"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
+ system_model "forgejo.org/models/system"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
)
// WarnAndNotice will log the provided message and send a repository notice
diff --git a/services/migrations/dump.go b/services/migrations/dump.go
index cb13cd3e5c..af161bc73d 100644
--- a/services/migrations/dump.go
+++ b/services/migrations/dump.go
@@ -16,16 +16,16 @@ import (
"strings"
"time"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
"github.com/google/uuid"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
)
var _ base.Uploader = &RepositoryDumper{}
diff --git a/services/migrations/forgejo_downloader.go b/services/migrations/forgejo_downloader.go
index 25dbb6ec51..5f809b82be 100644
--- a/services/migrations/forgejo_downloader.go
+++ b/services/migrations/forgejo_downloader.go
@@ -4,7 +4,7 @@
package migrations
import (
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/structs"
)
func init() {
diff --git a/services/migrations/forgejo_downloader_test.go b/services/migrations/forgejo_downloader_test.go
index 5bd37551cc..db1930ebba 100644
--- a/services/migrations/forgejo_downloader_test.go
+++ b/services/migrations/forgejo_downloader_test.go
@@ -6,7 +6,7 @@ package migrations
import (
"testing"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/structs"
"github.com/stretchr/testify/require"
)
diff --git a/services/migrations/git.go b/services/migrations/git.go
index 22ffd5e765..46710b0abe 100644
--- a/services/migrations/git.go
+++ b/services/migrations/git.go
@@ -6,7 +6,7 @@ package migrations
import (
"context"
- base "code.gitea.io/gitea/modules/migration"
+ base "forgejo.org/modules/migration"
)
var _ base.Downloader = &PlainGitDownloader{}
diff --git a/services/migrations/gitbucket.go b/services/migrations/gitbucket.go
index 4fe9e30a39..a9bd2dafb0 100644
--- a/services/migrations/gitbucket.go
+++ b/services/migrations/gitbucket.go
@@ -9,9 +9,9 @@ import (
"net/url"
"strings"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/structs"
)
var (
@@ -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, userName, password, token, repoOwner, repoName)
+ githubDownloader := NewGithubDownloaderV3(ctx, baseURL, true, true, 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
new file mode 100644
index 0000000000..3cb69d65c9
--- /dev/null
+++ b/services/migrations/gitbucket_test.go
@@ -0,0 +1,24 @@
+// 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.go b/services/migrations/gitea_downloader.go
index b42c7aa4da..133cc5c928 100644
--- a/services/migrations/gitea_downloader.go
+++ b/services/migrations/gitea_downloader.go
@@ -13,9 +13,9 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/structs"
gitea_sdk "code.gitea.io/sdk/gitea"
)
@@ -504,6 +504,28 @@ func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Com
return allComments, true, nil
}
+type ForgejoPullRequest struct {
+ gitea_sdk.PullRequest
+ Flow int64 `json:"flow"`
+}
+
+// Extracted from https://gitea.com/gitea/go-sdk/src/commit/164e3358bc02213954fb4380b821bed80a14824d/gitea/pull.go#L347-L364
+func (g *GiteaDownloader) fixPullHeadSha(pr *ForgejoPullRequest) error {
+ if pr.Base != nil && pr.Base.Repository != nil && pr.Base.Repository.Owner != nil && pr.Head != nil && pr.Head.Ref != "" && pr.Head.Sha == "" {
+ owner := pr.Base.Repository.Owner.UserName
+ repo := pr.Base.Repository.Name
+ refs, _, err := g.client.GetRepoRefs(owner, repo, pr.Head.Ref)
+ if err != nil {
+ return err
+ }
+ if len(refs) == 0 {
+ return fmt.Errorf("unable to resolve PR ref %q", pr.Head.Ref)
+ }
+ pr.Head.Sha = refs[0].Object.SHA
+ }
+ return nil
+}
+
// GetPullRequests returns pull requests according page and perPage
func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
if perPage > g.maxPerPage {
@@ -511,16 +533,30 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
}
allPRs := make([]*base.PullRequest, 0, perPage)
- prs, _, err := g.client.ListRepoPullRequests(g.repoOwner, g.repoName, gitea_sdk.ListPullRequestsOptions{
+ prs := make([]*ForgejoPullRequest, 0, perPage)
+ opt := gitea_sdk.ListPullRequestsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: perPage,
},
State: gitea_sdk.StateAll,
- })
+ }
+
+ link, _ := url.Parse(fmt.Sprintf("/repos/%s/%s/pulls", url.PathEscape(g.repoOwner), url.PathEscape(g.repoName)))
+ link.RawQuery = opt.QueryEncode()
+ _, err := getParsedResponse(g.client, "GET", link.String(), http.Header{"content-type": []string{"application/json"}}, nil, &prs)
if err != nil {
return nil, false, fmt.Errorf("error while listing pull requests (page: %d, pagesize: %d). Error: %w", page, perPage, err)
}
+
+ if g.client.CheckServerVersionConstraint(">= 1.14.0") != nil {
+ for i := range prs {
+ if err := g.fixPullHeadSha(prs[i]); err != nil {
+ return nil, false, fmt.Errorf("error while listing pull requests (page: %d, pagesize: %d). Error: %w", page, perPage, err)
+ }
+ }
+ }
+
for _, pr := range prs {
var milestone string
if pr.Milestone != nil {
@@ -598,6 +634,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
MergeCommitSHA: mergeCommitSHA,
IsLocked: pr.IsLocked,
PatchURL: pr.PatchURL,
+ Flow: pr.Flow,
Head: base.PullRequestBranch{
Ref: headRef,
SHA: headSHA,
diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go
index b9ddb9b431..5acc3b86a9 100644
--- a/services/migrations/gitea_downloader_test.go
+++ b/services/migrations/gitea_downloader_test.go
@@ -9,8 +9,8 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/unittest"
- base "code.gitea.io/gitea/modules/migration"
+ "forgejo.org/models/unittest"
+ base "forgejo.org/modules/migration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -45,7 +45,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
topics, err := downloader.GetTopics()
require.NoError(t, err)
sort.Strings(topics)
- assert.EqualValues(t, []string{"ci", "gitea", "migration", "test"}, topics)
+ assert.Equal(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.EqualValues(t, "open", issues[0].State)
+ assert.Equal(t, "open", issues[0].State)
issues, isEnd, err = downloader.GetIssues(3, 2)
require.NoError(t, err)
@@ -307,3 +307,46 @@ func TestGiteaDownloadRepo(t *testing.T) {
},
}, reviews)
}
+
+func TestForgejoDownloadRepo(t *testing.T) {
+ token := os.Getenv("CODE_FORGEJO_TOKEN")
+
+ fixturePath := "./testdata/code-forgejo-org/full_download"
+ server := unittest.NewMockWebServer(t, "https://code.forgejo.org", fixturePath, token != "")
+ defer server.Close()
+
+ downloader, err := NewGiteaDownloader(t.Context(), server.URL, "Gusted/agit-test", "", "", token)
+ require.NoError(t, err)
+ require.NotNil(t, downloader)
+
+ prs, _, err := downloader.GetPullRequests(1, 50)
+ require.NoError(t, err)
+ assert.Len(t, prs, 1)
+
+ assertPullRequestEqual(t, &base.PullRequest{
+ Number: 1,
+ PosterID: 63,
+ PosterName: "Gusted",
+ PosterEmail: "postmaster@gusted.xyz",
+ Title: "Add extra information",
+ State: "open",
+ Created: time.Date(2025, time.April, 1, 20, 28, 45, 0, time.UTC),
+ Updated: time.Date(2025, time.April, 1, 20, 28, 45, 0, time.UTC),
+ Base: base.PullRequestBranch{
+ CloneURL: "",
+ Ref: "main",
+ SHA: "79ebb873a6497c8847141ba9706b3f757196a1e6",
+ RepoName: "agit-test",
+ OwnerName: "Gusted",
+ },
+ Head: base.PullRequestBranch{
+ CloneURL: server.URL + "/Gusted/agit-test.git",
+ Ref: "refs/pull/1/head",
+ SHA: "667e9317ec37b977e6d3d7d43e3440636970563c",
+ RepoName: "agit-test",
+ OwnerName: "Gusted",
+ },
+ PatchURL: server.URL + "/Gusted/agit-test/pulls/1.patch",
+ Flow: 1,
+ }, prs[0])
+}
diff --git a/services/migrations/gitea_sdk_hack.go b/services/migrations/gitea_sdk_hack.go
new file mode 100644
index 0000000000..f3959717a8
--- /dev/null
+++ b/services/migrations/gitea_sdk_hack.go
@@ -0,0 +1,16 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package migrations
+
+import (
+ "io"
+ "net/http"
+
+ _ "unsafe" // Needed for go:linkname support
+
+ gitea_sdk "code.gitea.io/sdk/gitea"
+)
+
+//go:linkname getParsedResponse code.gitea.io/sdk/gitea.(*Client).getParsedResponse
+func getParsedResponse(client *gitea_sdk.Client, method, path string, header http.Header, body io.Reader, obj any) (*gitea_sdk.Response, error)
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 7bd6538ff2..7887dacdb1 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -14,26 +14,26 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- base_module "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/label"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/uri"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/pull"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ base_module "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/label"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/uri"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/pull"
+ repo_service "forgejo.org/services/repository"
"github.com/google/uuid"
)
@@ -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: prTitle,
+ Title: util.TruncateRunes(prTitle, 255),
Index: pr.Number,
Content: pr.Content,
MilestoneID: milestoneID,
@@ -802,6 +802,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model
MergeBase: pr.Base.SHA,
Index: pr.Number,
HasMerged: pr.Merged,
+ Flow: issues_model.PullRequestFlow(pr.Flow),
Issue: &issue,
}
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index e01f4664ba..e33d597cdc 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -12,24 +12,147 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/test"
+ "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"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"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()
@@ -40,9 +163,9 @@ func TestGiteaUploadRepo(t *testing.T) {
var (
ctx = t.Context()
- downloader = NewGithubDownloaderV3(ctx, "https://github.com", "", "", "", "go-xorm", "builder")
+ downloader = NewGithubDownloaderV3(ctx, "https://github.com", true, true, "", "", "", "go-xorm", "builder")
repoName = "builder-" + time.Now().Format("2006-01-02-15-04-05")
- uploader = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName)
+ uploader = NewGiteaLocalUploader(t.Context(), user, user.Name, repoName)
)
err := migrateRepository(db.DefaultContext, user, downloader, uploader, base.MigrateOptions{
@@ -64,7 +187,7 @@ func TestGiteaUploadRepo(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, Name: repoName})
assert.True(t, repo.HasWiki())
- assert.EqualValues(t, repo_model.RepositoryReady, repo.Status)
+ assert.Equal(t, repo_model.RepositoryReady, repo.Status)
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
RepoID: repo.ID,
@@ -173,7 +296,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
uploader.userMap = make(map[int64]int64)
err = uploader.remapUser(&source, &target)
require.NoError(t, err)
- assert.EqualValues(t, user.ID, target.GetUserID())
+ assert.Equal(t, user.ID, target.GetUserID())
}
func TestGiteaUploadRemapExternalUser(t *testing.T) {
@@ -224,7 +347,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
target = repo_model.Release{}
err = uploader.remapUser(&source, &target)
require.NoError(t, err)
- assert.EqualValues(t, linkedUser.ID, target.GetUserID())
+ assert.Equal(t, linkedUser.ID, target.GetUserID())
}
func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
@@ -504,14 +627,14 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
head, err := uploader.updateGitForPullRequest(&testCase.pr)
require.NoError(t, err)
- assert.EqualValues(t, testCase.head, head)
+ assert.Equal(t, testCase.head, head)
log.Info(stopMark)
logFiltered, logStopped := logChecker.Check(5 * time.Second)
assert.True(t, logStopped)
if len(testCase.logFilter) > 0 {
- assert.EqualValues(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
+ assert.Equal(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
}
})
}
diff --git a/services/migrations/github.go b/services/migrations/github.go
index 7025354f77..1fe5d2cc8e 100644
--- a/services/migrations/github.go
+++ b/services/migrations/github.go
@@ -14,11 +14,11 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/proxy"
+ "forgejo.org/modules/structs"
"github.com/google/go-github/v64/github"
"golang.org/x/oauth2"
@@ -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.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
+ return NewGithubDownloaderV3(ctx, baseURL, opts.PullRequests, opts.Issues, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
}
// GitServiceType returns the type of git service
@@ -69,30 +69,34 @@ 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
- 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
+ getPullRequests bool
+ getIssues bool
+ 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, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
+func NewGithubDownloaderV3(ctx context.Context, baseURL string, getPullRequests, getIssues bool, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
downloader := GithubDownloaderV3{
- userName: userName,
- baseURL: baseURL,
- password: password,
- ctx: ctx,
- repoOwner: repoOwner,
- repoName: repoName,
- maxPerPage: 100,
+ userName: userName,
+ baseURL: baseURL,
+ password: password,
+ ctx: ctx,
+ repoOwner: repoOwner,
+ repoName: repoName,
+ maxPerPage: 100,
+ getPullRequests: getPullRequests,
+ getIssues: getIssues,
}
if token != "" {
@@ -140,7 +144,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, _ = github.NewClient(client).WithEnterpriseURLs(baseURL, baseURL)
+ githubClient, _ = githubClient.WithEnterpriseURLs(baseURL, baseURL)
}
g.clients = append(g.clients, githubClient)
g.rates = append(g.rates, nil)
@@ -364,7 +368,8 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
// Prevent open redirect
if !hasBaseURL(redirectURL, g.baseURL) &&
- !hasBaseURL(redirectURL, "https://objects.githubusercontent.com/") {
+ !hasBaseURL(redirectURL, "https://objects.githubusercontent.com/") &&
+ !hasBaseURL(redirectURL, "https://release-assets.githubusercontent.com/") {
WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.GetID(), g, redirectURL)
return io.NopCloser(strings.NewReader(redirectURL)), nil
@@ -581,6 +586,24 @@ 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 (
@@ -607,6 +630,12 @@ 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 {
@@ -885,3 +914,18 @@ 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 080fd497ca..ef2850d2d2 100644
--- a/services/migrations/github_test.go
+++ b/services/migrations/github_test.go
@@ -9,22 +9,103 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/unittest"
- base "code.gitea.io/gitea/modules/migration"
+ "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, token != "")
+ server := unittest.NewMockWebServer(t, "https://api.github.com", fixturePath, false)
defer server.Close()
- downloader := NewGithubDownloaderV3(t.Context(), server.URL, "", "", token, "go-gitea", "test_repo")
+ downloader := NewGithubDownloaderV3(t.Context(), server.URL, true, true, "", "", token, "forgejo", "test_repo")
err := downloader.RefreshRate()
require.NoError(t, err)
@@ -32,38 +113,44 @@ func TestGitHubDownloadRepo(t *testing.T) {
require.NoError(t, err)
assertRepositoryEqual(t, &base.Repository{
Name: "test_repo",
- 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",
+ 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",
Website: "https://codeberg.org/forgejo/forgejo/",
}, repo)
topics, err := downloader.GetTopics()
require.NoError(t, err)
- assert.Contains(t, topics, "gitea")
+ assert.Contains(t, topics, "forgejo")
milestones, err := downloader.GetMilestones()
require.NoError(t, err)
assertMilestonesEqual(t, []*base.Milestone{
{
Title: "1.0.0",
- 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)),
+ 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)),
State: "closed",
},
{
Title: "1.1.0",
- 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",
+ 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",
},
}, milestones)
@@ -117,18 +204,34 @@ 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: "v0.9.99",
- TargetCommitish: "master",
+ TagName: "v1.0",
+ TargetCommitish: "main",
Name: "First Release",
- 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",
+ 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),
+ },
+ },
},
}, releases)
@@ -139,85 +242,41 @@ func TestGitHubDownloadRepo(t *testing.T) {
assertIssuesEqual(t, []*base.Issue{
{
Number: 1,
- 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: "bug",
- Color: "d73a4a",
- Description: "Something isn't working",
- },
- {
- Name: "good first issue",
- Color: "7057ff",
- Description: "Good for newcomers",
- },
- },
- Reactions: []*base.Reaction{
- {
- UserID: 1669571,
- UserName: "mrsdizzie",
- Content: "+1",
- },
- },
- Closed: timePtr(time.Date(2019, 11, 12, 20, 22, 22, 0, time.UTC)),
+ 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: "Test issue",
- Content: "This is test issue 2, do not touch!",
+ Title: "Second Issue",
+ Content: "Mentioning #1 ",
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),
+ 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),
Labels: []*base.Label{
{
Name: "duplicate",
Color: "cfd3d7",
Description: "This issue or pull request already exists",
},
- },
- Reactions: []*base.Reaction{
{
- UserID: 1669571,
- UserName: "mrsdizzie",
- Content: "heart",
+ Name: "good first issue",
+ Color: "7057ff",
+ Description: "Good for newcomers",
},
{
- 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",
+ Name: "help wanted",
+ Color: "008672",
+ Description: "Extra attention is needed",
},
},
- Closed: timePtr(time.Date(2019, 11, 12, 21, 1, 31, 0, time.UTC)),
},
}, issues)
@@ -227,26 +286,11 @@ func TestGitHubDownloadRepo(t *testing.T) {
assertCommentsEqual(t, []*base.Comment{
{
IssueIndex: 2,
- 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",
+ 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*",
Reactions: nil,
},
}, comments)
@@ -257,52 +301,50 @@ func TestGitHubDownloadRepo(t *testing.T) {
assertPullRequestsEqual(t, []*base.PullRequest{
{
Number: 3,
- 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),
+ 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),
Labels: []*base.Label{
{
- Name: "documentation",
- Color: "0075ca",
- Description: "Improvements or additions to documentation",
+ Name: "enhancement",
+ Color: "a2eeef",
+ Description: "New feature or request",
},
},
- PatchURL: server.URL + "/go-gitea/test_repo/pull/3.patch",
+ PatchURL: server.URL + "/forgejo/test_repo/pull/3.patch",
Head: base.PullRequestBranch{
- Ref: "master",
- CloneURL: server.URL + "/mrsdizzie/test_repo.git",
- SHA: "076160cf0b039f13e5eff19619932d181269414b",
+ Ref: "some-feature",
+ CloneURL: server.URL + "/forgejo/test_repo.git",
+ SHA: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
RepoName: "test_repo",
- OwnerName: "mrsdizzie",
+ OwnerName: "forgejo",
},
Base: base.PullRequestBranch{
- Ref: "master",
- SHA: "72866af952e98d02a73003501836074b286a78f6",
- OwnerName: "go-gitea",
+ Ref: "main",
+ SHA: "442d28a55b842472c95bead51a4c61f209ac1636",
+ OwnerName: "forgejo",
RepoName: "test_repo",
},
- 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,
+ ForeignIndex: 3,
},
{
- Number: 4,
- Title: "Test branch",
- Content: "do not merge this PR",
+ Number: 7,
+ Title: "Update readme.md",
+ Content: "Adding some text to the readme",
Milestone: "1.0.0",
- 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),
+ 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)),
Labels: []*base.Label{
{
Name: "bug",
@@ -310,35 +352,23 @@ func TestGitHubDownloadRepo(t *testing.T) {
Description: "Something isn't working",
},
},
- PatchURL: server.URL + "/go-gitea/test_repo/pull/4.patch",
+ PatchURL: server.URL + "/forgejo/test_repo/pull/7.patch",
Head: base.PullRequestBranch{
- Ref: "test-branch",
- SHA: "2be9101c543658591222acbee3eb799edfc3853d",
+ Ref: "another-feature",
+ SHA: "5638cb8f3278e467fc1eefcac14d3c0d5d91601f",
RepoName: "test_repo",
- OwnerName: "mrsdizzie",
- CloneURL: server.URL + "/mrsdizzie/test_repo.git",
+ OwnerName: "forgejo",
+ CloneURL: server.URL + "/forgejo/test_repo.git",
},
Base: base.PullRequestBranch{
- Ref: "master",
- SHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
- OwnerName: "go-gitea",
+ Ref: "main",
+ SHA: "6dd0c6801ddbb7333787e73e99581279492ff449",
+ OwnerName: "forgejo",
RepoName: "test_repo",
},
- Merged: false,
- MergeCommitSHA: "565d1208f5fffdc1c5ae1a2436491eb9a5e4ebae",
- Reactions: []*base.Reaction{
- {
- UserID: 81045,
- UserName: "lunny",
- Content: "heart",
- },
- {
- UserID: 81045,
- UserName: "lunny",
- Content: "+1",
- },
- },
- ForeignIndex: 4,
+ Merged: true,
+ MergeCommitSHA: "ca43b48ca2c461f9a5cb66500a154b23d07c9f90",
+ ForeignIndex: 7,
},
}, prs)
@@ -346,90 +376,85 @@ func TestGitHubDownloadRepo(t *testing.T) {
require.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
- ID: 315859956,
+ ID: 3096999684,
IssueIndex: 3,
- 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,
+ ReviewerID: 37243484,
+ ReviewerName: "PatDyn",
+ CommitID: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
+ CreatedAt: time.Date(2025, 8, 7, 12, 47, 55, 0, time.UTC),
+ State: base.ReviewStateCommented,
Comments: []*base.ReviewComment{
{
- 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: 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: 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),
+ ID: 3097007243,
+ IssueIndex: 3,
+ ReviewerID: 37243484,
+ ReviewerName: "PatDyn",
+ CommitID: "c608ab3997349219e1510cdb5ddd1e5e82897dfa",
+ CreatedAt: time.Date(2025, 8, 7, 12, 49, 36, 0, time.UTC),
State: base.ReviewStateCommented,
Comments: []*base.ReviewComment{
{
- 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),
+ 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),
},
},
},
}, 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 2eb3e6629d..f54f682c47 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -15,12 +15,12 @@ import (
"strings"
"time"
- issues_model "code.gitea.io/gitea/models/issues"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/structs"
+ issues_model "forgejo.org/models/issues"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/structs"
gitlab "gitlab.com/gitlab-org/api/client-go"
)
@@ -99,6 +99,7 @@ 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()))
}
@@ -213,7 +214,7 @@ func (g *GitlabDownloader) GetTopics() ([]string, error) {
if err != nil {
return nil, err
}
- return gr.TagList, err
+ return gr.Topics, err
}
// GetMilestones returns milestones
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index f1404d946d..30b24f09e8 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -12,9 +12,9 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/json"
- base "code.gitea.io/gitea/modules/migration"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/json"
+ base "forgejo.org/modules/migration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -49,7 +49,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
topics, err := downloader.GetTopics()
require.NoError(t, err)
assert.Len(t, topics, 2)
- assert.EqualValues(t, []string{"migration", "test"}, topics)
+ assert.Equal(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.EqualValues(t, "vpn unlimited errors", issues[0].Title)
+ assert.Equal(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.EqualValues(t, "Review", prs[0].Title)
+ assert.Equal(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.EqualValues(t, []*base.Reaction{
+ assert.Equal(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.EqualValues(t, actualComment, comments[i])
+ assert.Equal(t, actualComment, comments[i])
}
}
diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go
index 1fef4808b0..b6fb8cef0a 100644
--- a/services/migrations/gogs.go
+++ b/services/migrations/gogs.go
@@ -11,10 +11,10 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/proxy"
+ "forgejo.org/modules/structs"
"github.com/gogs/go-gogs-client"
)
diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go
index 450aeab5ef..bf0d063ca4 100644
--- a/services/migrations/gogs_test.go
+++ b/services/migrations/gogs_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"time"
- base "code.gitea.io/gitea/modules/migration"
+ base "forgejo.org/modules/migration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.Skipf("visit test repo failed, ignored")
+ t.Skip("visit test repo failed, ignored")
return
}
@@ -215,9 +215,9 @@ func TestGogsDownloaderFactory_New(t *testing.T) {
}
assert.IsType(t, &GogsDownloader{}, got)
- assert.EqualValues(t, tt.baseURL, got.(*GogsDownloader).baseURL)
- assert.EqualValues(t, tt.repoOwner, got.(*GogsDownloader).repoOwner)
- assert.EqualValues(t, tt.repoName, got.(*GogsDownloader).repoName)
+ assert.Equal(t, tt.baseURL, got.(*GogsDownloader).baseURL)
+ assert.Equal(t, tt.repoOwner, got.(*GogsDownloader).repoOwner)
+ assert.Equal(t, tt.repoName, got.(*GogsDownloader).repoName)
})
}
}
diff --git a/services/migrations/http_client.go b/services/migrations/http_client.go
index 0b997e08f4..26962f2976 100644
--- a/services/migrations/http_client.go
+++ b/services/migrations/http_client.go
@@ -7,9 +7,9 @@ import (
"crypto/tls"
"net/http"
- "code.gitea.io/gitea/modules/hostmatcher"
- "code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/hostmatcher"
+ "forgejo.org/modules/proxy"
+ "forgejo.org/modules/setting"
)
// NewMigrationHTTPClient returns a HTTP client for migration
diff --git a/services/migrations/main_test.go b/services/migrations/main_test.go
index f78d75e4db..d543bd6d9c 100644
--- a/services/migrations/main_test.go
+++ b/services/migrations/main_test.go
@@ -8,8 +8,8 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/unittest"
- base "code.gitea.io/gitea/modules/migration"
+ "forgejo.org/models/unittest"
+ base "forgejo.org/modules/migration"
"github.com/stretchr/testify/assert"
)
@@ -136,6 +136,7 @@ func assertPullRequestEqual(t *testing.T, expected, actual *base.PullRequest) {
assert.ElementsMatch(t, expected.Assignees, actual.Assignees)
assert.Equal(t, expected.IsLocked, actual.IsLocked)
assertReactionsEqual(t, expected.Reactions, actual.Reactions)
+ assert.Equal(t, expected.Flow, actual.Flow)
}
func assertPullRequestsEqual(t *testing.T, expected, actual []*base.PullRequest) {
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index ccb9cb7e98..61630d9c6d 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -6,22 +6,23 @@ package migrations
import (
"context"
+ "errors"
"fmt"
"net"
"net/url"
"path/filepath"
"strings"
- "code.gitea.io/gitea/models"
- repo_model "code.gitea.io/gitea/models/repo"
- system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/hostmatcher"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models"
+ repo_model "forgejo.org/models/repo"
+ system_model "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/hostmatcher"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
)
// MigrateOptions is equal to base.MigrateOptions
@@ -227,7 +228,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
if cloneURL.Scheme == "file" || cloneURL.Scheme == "" {
if cloneAddrURL.Scheme != "file" && cloneAddrURL.Scheme != "" {
- return fmt.Errorf("repo info has changed from external to local filesystem")
+ return errors.New("repo info has changed from external to local filesystem")
}
}
diff --git a/services/migrations/migrate_test.go b/services/migrations/migrate_test.go
index 6e45cbd906..804d01df7a 100644
--- a/services/migrations/migrate_test.go
+++ b/services/migrations/migrate_test.go
@@ -8,9 +8,9 @@ import (
"path/filepath"
"testing"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/require"
)
diff --git a/services/migrations/onedev.go b/services/migrations/onedev.go
index e2f7b771f3..a553a4d8f5 100644
--- a/services/migrations/onedev.go
+++ b/services/migrations/onedev.go
@@ -12,10 +12,10 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/structs"
)
var (
diff --git a/services/migrations/onedev_test.go b/services/migrations/onedev_test.go
index 46e3eb8d18..5bb2e2bb5c 100644
--- a/services/migrations/onedev_test.go
+++ b/services/migrations/onedev_test.go
@@ -9,7 +9,7 @@ import (
"testing"
"time"
- base "code.gitea.io/gitea/modules/migration"
+ base "forgejo.org/modules/migration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/migrations/pagure.go b/services/migrations/pagure.go
new file mode 100644
index 0000000000..f9433671c0
--- /dev/null
+++ b/services/migrations/pagure.go
@@ -0,0 +1,600 @@
+// 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
new file mode 100644
index 0000000000..22b2eed85e
--- /dev/null
+++ b/services/migrations/pagure_test.go
@@ -0,0 +1,637 @@
+// 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 e8725bc647..e8a1e0fdcf 100644
--- a/services/migrations/restore.go
+++ b/services/migrations/restore.go
@@ -10,9 +10,9 @@ import (
"path/filepath"
"strconv"
- base "code.gitea.io/gitea/modules/migration"
+ base "forgejo.org/modules/migration"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
)
// RepositoryRestorer implements an Downloader from the local directory
diff --git a/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Frepos%2FGusted%2Fagit-test%2Fpulls%3Flimit=50&page=1&state=all b/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Frepos%2FGusted%2Fagit-test%2Fpulls%3Flimit=50&page=1&state=all
new file mode 100644
index 0000000000..87095d9e24
--- /dev/null
+++ b/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Frepos%2FGusted%2Fagit-test%2Fpulls%3Flimit=50&page=1&state=all
@@ -0,0 +1,8 @@
+Access-Control-Expose-Headers: X-Total-Count
+Cache-Control: max-age=0, private, must-revalidate, no-transform
+Content-Type: application/json;charset=utf-8
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+X-Total-Count: 1
+
+[{"id":4980,"url":"https://code.forgejo.org/Gusted/agit-test/pulls/1","number":1,"user":{"id":63,"login":"Gusted","login_name":"26734","source_id":1,"full_name":"","email":"postmaster@gusted.xyz","avatar_url":"https://code.forgejo.org/avatars/4ca5ad8bc488630869fdbd2051da61cbed7241c9c066d4e5e1dd36300f887340","html_url":"https://code.forgejo.org/Gusted","language":"en-US","is_admin":false,"last_login":"2025-04-01T16:35:18Z","created":"2023-07-08T13:33:38Z","restricted":false,"active":true,"prohibit_login":false,"location":"","pronouns":"","website":"","description":"","visibility":"public","followers_count":2,"following_count":0,"starred_repos_count":0,"username":"Gusted"},"title":"Add extra information","body":"","labels":[],"milestone":null,"assignee":null,"assignees":null,"requested_reviewers":[],"requested_reviewers_teams":[],"state":"open","draft":false,"is_locked":false,"comments":0,"review_comments":0,"additions":0,"deletions":0,"changed_files":0,"html_url":"https://code.forgejo.org/Gusted/agit-test/pulls/1","diff_url":"https://code.forgejo.org/Gusted/agit-test/pulls/1.diff","patch_url":"https://code.forgejo.org/Gusted/agit-test/pulls/1.patch","mergeable":true,"merged":false,"merged_at":null,"merge_commit_sha":null,"merged_by":null,"allow_maintainer_edit":false,"base":{"label":"main","ref":"main","sha":"79ebb873a6497c8847141ba9706b3f757196a1e6","repo_id":1414,"repo":{"id":1414,"owner":{"id":63,"login":"Gusted","login_name":"","source_id":0,"full_name":"","email":"gusted@noreply.code.forgejo.org","avatar_url":"https://code.forgejo.org/avatars/4ca5ad8bc488630869fdbd2051da61cbed7241c9c066d4e5e1dd36300f887340","html_url":"https://code.forgejo.org/Gusted","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2023-07-08T13:33:38Z","restricted":false,"active":false,"prohibit_login":false,"location":"","pronouns":"","website":"","description":"","visibility":"public","followers_count":2,"following_count":0,"starred_repos_count":0,"username":"Gusted"},"name":"agit-test","full_name":"Gusted/agit-test","description":"USED FOR FORGEJO UNIT TESTING","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":36,"language":"","languages_url":"https://code.forgejo.org/api/v1/repos/Gusted/agit-test/languages","html_url":"https://code.forgejo.org/Gusted/agit-test","url":"https://code.forgejo.org/api/v1/repos/Gusted/agit-test","link":"","ssh_url":"ssh://git@code.forgejo.org/Gusted/agit-test.git","clone_url":"https://code.forgejo.org/Gusted/agit-test.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2025-04-01T20:25:03Z","updated_at":"2025-04-01T20:25:03Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"wiki_branch":"main","globally_editable_wiki":false,"has_pull_requests":true,"has_projects":true,"has_releases":true,"has_packages":true,"has_actions":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":true,"allow_rebase_update":true,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"default_update_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null,"topics":null}},"head":{"label":"","ref":"refs/pull/1/head","sha":"667e9317ec37b977e6d3d7d43e3440636970563c","repo_id":1414,"repo":{"id":1414,"owner":{"id":63,"login":"Gusted","login_name":"","source_id":0,"full_name":"","email":"gusted@noreply.code.forgejo.org","avatar_url":"https://code.forgejo.org/avatars/4ca5ad8bc488630869fdbd2051da61cbed7241c9c066d4e5e1dd36300f887340","html_url":"https://code.forgejo.org/Gusted","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2023-07-08T13:33:38Z","restricted":false,"active":false,"prohibit_login":false,"location":"","pronouns":"","website":"","description":"","visibility":"public","followers_count":2,"following_count":0,"starred_repos_count":0,"username":"Gusted"},"name":"agit-test","full_name":"Gusted/agit-test","description":"USED FOR FORGEJO UNIT TESTING","empty":false,"private":false,"fork":false,"template":false,"parent":null,"mirror":false,"size":36,"language":"","languages_url":"https://code.forgejo.org/api/v1/repos/Gusted/agit-test/languages","html_url":"https://code.forgejo.org/Gusted/agit-test","url":"https://code.forgejo.org/api/v1/repos/Gusted/agit-test","link":"","ssh_url":"ssh://git@code.forgejo.org/Gusted/agit-test.git","clone_url":"https://code.forgejo.org/Gusted/agit-test.git","original_url":"","website":"","stars_count":0,"forks_count":0,"watchers_count":1,"open_issues_count":0,"open_pr_counter":1,"release_counter":0,"default_branch":"main","archived":false,"created_at":"2025-04-01T20:25:03Z","updated_at":"2025-04-01T20:25:03Z","archived_at":"1970-01-01T00:00:00Z","permissions":{"admin":true,"push":true,"pull":true},"has_issues":true,"internal_tracker":{"enable_time_tracker":true,"allow_only_contributors_to_track_time":true,"enable_issue_dependencies":true},"has_wiki":true,"wiki_branch":"main","globally_editable_wiki":false,"has_pull_requests":true,"has_projects":true,"has_releases":true,"has_packages":true,"has_actions":true,"ignore_whitespace_conflicts":false,"allow_merge_commits":true,"allow_rebase":true,"allow_rebase_explicit":true,"allow_squash_merge":true,"allow_fast_forward_only_merge":true,"allow_rebase_update":true,"default_delete_branch_after_merge":false,"default_merge_style":"merge","default_allow_maintainer_edit":false,"default_update_style":"merge","avatar_url":"","internal":false,"mirror_interval":"","object_format_name":"sha1","mirror_updated":"0001-01-01T00:00:00Z","repo_transfer":null,"topics":null}},"merge_base":"79ebb873a6497c8847141ba9706b3f757196a1e6","due_date":null,"created_at":"2025-04-01T20:28:45Z","updated_at":"2025-04-01T20:28:45Z","closed_at":null,"pin_order":0,"flow":1}]
diff --git a/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Fsettings%2Fapi b/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Fsettings%2Fapi
new file mode 100644
index 0000000000..11c4e7b8ba
--- /dev/null
+++ b/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Fsettings%2Fapi
@@ -0,0 +1,7 @@
+Content-Length: 117
+Cache-Control: max-age=0, private, must-revalidate, no-transform
+Content-Type: application/json;charset=utf-8
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+
+{"max_response_items":50,"default_paging_num":30,"default_git_trees_per_page":1000,"default_max_blob_size":10485760}
diff --git a/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Fversion b/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Fversion
new file mode 100644
index 0000000000..411ed84e24
--- /dev/null
+++ b/services/migrations/testdata/code-forgejo-org/full_download/GET_%2Fapi%2Fv1%2Fversion
@@ -0,0 +1,7 @@
+Cache-Control: max-age=0, private, must-revalidate, no-transform
+Content-Type: application/json;charset=utf-8
+X-Content-Type-Options: nosniff
+X-Frame-Options: SAMEORIGIN
+Content-Length: 53
+
+{"version":"11.0.0-dev-617-1d1e0ced3e+gitea-1.22.0"}
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frate_limit b/services/migrations/testdata/github/full_download/GET_%2Frate_limit
index 74e43a0765..f3a1c10f1d 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frate_limit
+++ b/services/migrations/testdata/github/full_download/GET_%2Frate_limit
@@ -1,23 +1,24 @@
Cache-Control: no-cache
+X-Ratelimit-Used: 136
+X-Ratelimit-Resource: core
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-X-Ratelimit-Reset: 1730800941
-Access-Control-Allow-Origin: *
-Content-Type: application/json; charset=utf-8
-X-Oauth-Scopes:
-X-Github-Media-Type: github.v3; format=json
-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
+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
-X-Ratelimit-Used: 101
-X-Ratelimit-Resource: core
+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
+Access-Control-Allow-Origin: *
+X-Content-Type-Options: nosniff
+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-Xss-Protection: 0
+Content-Security-Policy: default-src 'none'
-{"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
+{"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
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
new file mode 100644
index 0000000000..57e87a4775
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo
@@ -0,0 +1,26 @@
+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%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
index a7a105b3e7..2af575abc9 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
@@ -1,25 +1,26 @@
-X-Ratelimit-Used: 88
-X-Ratelimit-Resource: core
-X-Content-Type-Options: nosniff
+X-Github-Api-Version-Selected: 2022-11-28
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: *
+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
X-Xss-Protection: 0
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
-X-Oauth-Scopes:
-X-Accepted-Oauth-Scopes: repo
-X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Remaining: 4912
-Content-Length: 2
+Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
[]
\ 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
new file mode 100644
index 0000000000..5a45f886a9
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created
@@ -0,0 +1,25 @@
+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%2F2%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2
new file mode 100644
index 0000000000..ecaafa82e4
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2
@@ -0,0 +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
+X-Ratelimit-Limit: 5000
+Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
+X-Ratelimit-Remaining: 4874
+X-Ratelimit-Used: 126
+X-Ratelimit-Resource: core
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+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%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
new file mode 100644
index 0000000000..a900075c9b
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=1&per_page=2
@@ -0,0 +1,26 @@
+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-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
+X-Xss-Protection: 0
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
+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
+
+[]
\ 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=2&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=2&per_page=2
similarity index 61%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=2&per_page=2
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F3%2Freactions%3Fpage=2&per_page=2
index 79b506ea55..7d978a2584 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%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
@@ -1,26 +1,27 @@
-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
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
-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-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
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-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
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%2F7%2Freactions%3Fpage=1&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F7%2Freactions%3Fpage=1&per_page=2
new file mode 100644
index 0000000000..694c612697
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2F7%2Freactions%3Fpage=1&per_page=2
@@ -0,0 +1,26 @@
+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-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
+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'
+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
+Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
+
+[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2Fcomments%2F3164123494%2Freactions%3Fpage=1&per_page=100
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage=1&per_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2Fcomments%2F3164123494%2Freactions%3Fpage=1&per_page=100
index ac446b3586..4ee1c6bc52 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553138856%2Freactions%3Fpage=1&per_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%2Fcomments%2F3164123494%2Freactions%3Fpage=1&per_page=100
@@ -1,25 +1,26 @@
-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
-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
-Content-Length: 2
-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
+Content-Type: application/json; charset=utf-8
+X-Oauth-Scopes: public_repo, repo:status
+X-Ratelimit-Remaining: 4872
+X-Frame-Options: deny
+X-Xss-Protection: 0
Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Oauth-Scopes:
-X-Ratelimit-Used: 86
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+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: *
[]
\ 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
new file mode 100644
index 0000000000..6e74d44a20
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fissues%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
@@ -0,0 +1,26 @@
+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
new file mode 100644
index 0000000000..ef1aebf7a8
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Flabels%3Fpage=1&per_page=100
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000000..7dba1e4aac
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all
@@ -0,0 +1,25 @@
+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%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
similarity index 75%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
index 1b4481f890..5d382a0ed1 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Frequested_reviewers%3Fper_page=100
@@ -1,24 +1,25 @@
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-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
+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:
-X-Ratelimit-Remaining: 4905
-X-Ratelimit-Reset: 1730800941
-X-Ratelimit-Used: 95
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+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-Ratelimit-Limit: 5000
+X-Ratelimit-Remaining: 4863
+X-Ratelimit-Reset: 1755007969
+X-Ratelimit-Used: 137
+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
{"users":[],"teams":[]}
\ 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
new file mode 100644
index 0000000000..51f70d2a81
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3096999684%2Fcomments%3Fper_page=100
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000000..cd5fbdaf9d
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%2F3097007243%2Fcomments%3Fper_page=100
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000000..82970c7a67
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100
@@ -0,0 +1,25 @@
+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%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
similarity index 63%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315860062%2Fcomments%3Fper_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
index 389c1b7567..eea4344781 100644
--- 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%2Fforgejo%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
@@ -1,25 +1,23 @@
-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
+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
-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
+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
-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
+{"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%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Frequested_reviewers%3Fper_page=100
similarity index 75%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers%3Fper_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Frequested_reviewers%3Fper_page=100
index 676e326094..f3bef5c3bd 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Frequested_reviewers%3Fper_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Frequested_reviewers%3Fper_page=100
@@ -1,24 +1,25 @@
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-X-Oauth-Scopes:
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Remaining: 4898
+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
-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
+X-Github-Request-Id: F852:1553BC:10F18A:FD450:689B3DFF
+Content-Type: application/json; charset=utf-8
+Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
+X-Ratelimit-Limit: 5000
{"users":[],"teams":[]}
\ 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%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Freviews%3Fper_page=100
similarity index 75%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315861440%2Fcomments%3Fper_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Freviews%3Fper_page=100
index e52428a5af..fea2907aa4 100644
--- 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%2Fforgejo%2Ftest_repo%2Fpulls%2F7%2Freviews%3Fper_page=100
@@ -1,25 +1,26 @@
-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
+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-Frame-Options: deny
+Content-Security-Policy: default-src 'none'
Content-Length: 2
-X-Oauth-Scopes:
+X-Accepted-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-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
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Content-Type-Options: nosniff
[]
\ No newline at end of file
diff --git a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260216729%2Freactions%3Fpage=1&per_page=100
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage=1&per_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260216729%2Freactions%3Fpage=1&per_page=100
index 748ae93381..39a6e753b9 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363017488%2Freactions%3Fpage=1&per_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260216729%2Freactions%3Fpage=1&per_page=100
@@ -1,25 +1,26 @@
-Vary: Accept, Authorization, Cookie, X-GitHub-OTP,Accept-Encoding, Accept, X-Requested-With
-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
-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: *
-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:
+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
-X-Ratelimit-Used: 98
+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
+X-Accepted-Oauth-Scopes:
+X-Github-Media-Type: github.v3; param=squirrel-girl-preview
+X-Ratelimit-Limit: 5000
+X-Ratelimit-Reset: 1755007969
+X-Ratelimit-Used: 134
+Access-Control-Allow-Origin: *
+Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
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%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage=1&per_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260221159%2Freactions%3Fpage=1&per_page=100
similarity index 76%
rename from services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage=1&per_page=100
rename to services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260221159%2Freactions%3Fpage=1&per_page=100
index 0b0ae88deb..1c80bb32d7 100644
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2Fcomments%2F363029944%2Freactions%3Fpage=1&per_page=100
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%2Fcomments%2F2260221159%2Freactions%3Fpage=1&per_page=100
@@ -1,25 +1,26 @@
-X-Accepted-Oauth-Scopes:
-X-Ratelimit-Remaining: 4899
-Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
-Content-Length: 2
-X-Ratelimit-Used: 101
-Access-Control-Allow-Origin: *
+X-Ratelimit-Used: 136
X-Frame-Options: deny
-X-Content-Type-Options: nosniff
-X-Xss-Protection: 0
+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
+Access-Control-Allow-Origin: *
Content-Security-Policy: default-src 'none'
Cache-Control: private, max-age=60, s-maxage=60
-Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
-X-Oauth-Scopes:
+X-Oauth-Scopes: public_repo, repo:status
+Github-Authentication-Token-Expiration: 2025-09-11 10:37:11 UTC
X-Github-Api-Version-Selected: 2022-11-28
-X-Ratelimit-Limit: 5000
-X-Ratelimit-Reset: 1730800941
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-Content-Type: application/json; charset=utf-8
+X-Content-Type-Options: nosniff
+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
+Etag: "578cfdf865ccd59faa414c655476c87af814e220755c938b9545967521b6f14b"
+X-Ratelimit-Remaining: 4864
+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: C7CC:3118FC:3F622AC:4038BA5:6729E6C0
+Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
+X-Xss-Protection: 0
+X-Accepted-Oauth-Scopes:
+X-Ratelimit-Limit: 5000
[]
\ 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
new file mode 100644
index 0000000000..2835f22e9b
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Fpulls%3Fdirection=asc&page=1&per_page=2&sort=created&state=all
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000000..5fc5f2dc94
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fforgejo%2Ftest_repo%2Freleases%3Fpage=1&per_page=100
@@ -0,0 +1,25 @@
+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
deleted file mode 100644
index 78fde4d424..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index f1f9afee15..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=1&per_page=2
+++ /dev/null
@@ -1,24 +0,0 @@
-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%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=2&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=2&per_page=2
deleted file mode 100644
index fe993d3c3b..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F1%2Freactions%3Fpage=2&per_page=2
+++ /dev/null
@@ -1,26 +0,0 @@
-Etag: "f87b0fd59e59458a9a808311324b873c6baf3fde861298a99de9986270dd4d79"
-X-Github-Media-Type: github.v3; param=squirrel-girl-preview
-X-Github-Api-Version-Selected: 2022-11-28
-Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
-X-Content-Type-Options: nosniff
-Content-Type: application/json; charset=utf-8
-Content-Length: 2
-Cache-Control: private, max-age=60, s-maxage=60
-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
-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
deleted file mode 100644
index 61867c5ae6..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Fcomments%3Fdirection=asc&per_page=100&sort=created
+++ /dev/null
@@ -1,24 +0,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-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
deleted file mode 100644
index bb9dea395c..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=1&per_page=2
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index e59fc93546..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=2&per_page=2
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index 57f03c8a64..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=3&per_page=2
+++ /dev/null
@@ -1,25 +0,0 @@
-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%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=4&per_page=2 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=4&per_page=2
deleted file mode 100644
index f14d4ba904..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F2%2Freactions%3Fpage=4&per_page=2
+++ /dev/null
@@ -1,26 +0,0 @@
-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
-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'
-
-[]
\ 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
deleted file mode 100644
index f5398c3a9f..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2F4%2Freactions%3Fpage=1&per_page=2
+++ /dev/null
@@ -1,24 +0,0 @@
-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%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
deleted file mode 100644
index b55068a718..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=1&per_page=100
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index 1e46f438e5..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fissues%2Fcomments%2F553111966%2Freactions%3Fpage=2&per_page=100
+++ /dev/null
@@ -1,26 +0,0 @@
-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%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
deleted file mode 100644
index 80d2f90dbc..0000000000
--- 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
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index f1d483aacb..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Flabels%3Fpage=1&per_page=100
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index 50b90ad4ab..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fmilestones%3Fpage=1&per_page=100&state=all
+++ /dev/null
@@ -1,24 +0,0 @@
-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%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page=100
deleted file mode 100644
index 435e1a0ee0..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%2F315859956%2Fcomments%3Fper_page=100
+++ /dev/null
@@ -1,25 +0,0 @@
-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
-X-Content-Type-Options: nosniff
-Content-Length: 2
-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-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
-X-Accepted-Oauth-Scopes:
-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'
-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%3Fper_page=100 b/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100
deleted file mode 100644
index 203c363ffa..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F3%2Freviews%3Fper_page=100
+++ /dev/null
@@ -1,24 +0,0 @@
-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%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
deleted file mode 100644
index 48e5b2c3d9..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338338740%2Fcomments%3Fper_page=100
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index 4cc66424f0..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338339651%2Fcomments%3Fper_page=100
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index f13d4addc7..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%2F338349019%2Fcomments%3Fper_page=100
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index c4484e078a..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Fpulls%2F4%2Freviews%3Fper_page=100
+++ /dev/null
@@ -1,24 +0,0 @@
-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%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
deleted file mode 100644
index 30883cc283..0000000000
--- 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
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index 470f5c5769..0000000000
--- a/services/migrations/testdata/github/full_download/GET_%2Frepos%2Fgo-gitea%2Ftest_repo%2Freleases%3Fpage=1&per_page=100
+++ /dev/null
@@ -1,24 +0,0 @@
-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
new file mode 100644
index 0000000000..b870d82622
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/HEAD
@@ -0,0 +1 @@
+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
new file mode 100644
index 0000000000..07d359d07c
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/config
@@ -0,0 +1,4 @@
+[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
new file mode 100644
index 0000000000..498b267a8c
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/description
@@ -0,0 +1 @@
+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
new file mode 100644
index 0000000000..a5196d1be8
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/exclude
@@ -0,0 +1,6 @@
+# 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
new file mode 100644
index 0000000000..07fe1b45cd
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/info/refs
@@ -0,0 +1 @@
+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
new file mode 100644
index 0000000000..5a598008b9
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/17/58ff42c9ab82988ad33d211ef5433afd779a9e 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
new file mode 100644
index 0000000000..e296f574da
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/1a/0f6de7bb300de4a569371cfedf1c6ff189d374 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
new file mode 100644
index 0000000000..f7aae32db0
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/44/2d28a55b842472c95bead51a4c61f209ac1636 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
new file mode 100644
index 0000000000..b7d43b3d6d
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/56/38cb8f3278e467fc1eefcac14d3c0d5d91601f 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
new file mode 100644
index 0000000000..a995c75bf7
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/59/93b8814449c4cc660132613c12cf6b26c9824c 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
new file mode 100644
index 0000000000..aaccbfefb4
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/6d/d0c6801ddbb7333787e73e99581279492ff449 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
new file mode 100644
index 0000000000..2dfcea6cd3
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/76/5f7381bf10911971845cc881fa54a93389a270 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
new file mode 100644
index 0000000000..0fe680ed57
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7c/93d105d3ee6af50705c4d2cc8dd548c2f9edc9 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
new file mode 100644
index 0000000000..385dfc7fe7
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/7d/c3f365f3bc83011f0df5ee8637de7dd9487c04
@@ -0,0 +1,3 @@
+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
new file mode 100644
index 0000000000..64a7a811da
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/9b/fcee0cb75322db96a76b49faa222e2c59f485d 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
new file mode 100644
index 0000000000..c39c6e8a75
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ba/96372ae89cfd798c343542d898da029e2d8a02
@@ -0,0 +1,2 @@
+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
new file mode 100644
index 0000000000..e92cf50fa5
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/bf/2e0fd70c6ef1ab5240a340ba38eb47b3e8409f 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
new file mode 100644
index 0000000000..467ab42ca3
Binary files /dev/null and b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/objects/ca/43b48ca2c461f9a5cb66500a154b23d07c9f90 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
new file mode 100644
index 0000000000..fe8dc33916
--- /dev/null
+++ b/services/migrations/testdata/github/full_download/forgejo/test_repo.git/refs/heads/main
@@ -0,0 +1 @@
+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
new file mode 100644
index 0000000000..d1693ef67f
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce
@@ -0,0 +1,84 @@
+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
new file mode 100644
index 0000000000..063950557b
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000000..dbd4992dbd
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000000..ce899b5e29
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000000..412298b22c
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000000..6ac5ccf674
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000000..3505297407
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo
@@ -0,0 +1,24 @@
+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
new file mode 100644
index 0000000000..a5116fa017
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F3
@@ -0,0 +1,16 @@
+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
new file mode 100644
index 0000000000..e4dbc1976d
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F4
@@ -0,0 +1,16 @@
+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
new file mode 100644
index 0000000000..e04d6fd928
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues
@@ -0,0 +1,298 @@
+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
new file mode 100644
index 0000000000..9c2bd27396
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F10
@@ -0,0 +1,258 @@
+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
new file mode 100644
index 0000000000..3fc0a4be13
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5
@@ -0,0 +1,248 @@
+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
new file mode 100644
index 0000000000..46a27d2130
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F6
@@ -0,0 +1,248 @@
+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
new file mode 100644
index 0000000000..0693dc39f3
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F7
@@ -0,0 +1,233 @@
+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
new file mode 100644
index 0000000000..f5a3ad4712
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F8
@@ -0,0 +1,233 @@
+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
new file mode 100644
index 0000000000..f65e51e90d
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F9
@@ -0,0 +1,238 @@
+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
new file mode 100644
index 0000000000..a047968f02
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests
@@ -0,0 +1,506 @@
+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
new file mode 100644
index 0000000000..5afe87654e
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000000..37d40b9c6e
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000000..4f170a50fe
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000000..fb79ce7a97
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000000..5e968f23b9
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000000..83d48ee851
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000000..85a07333f8
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000000..131f4b34a8
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/authorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000000..ef69d4bdb8
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce
@@ -0,0 +1,82 @@
+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
new file mode 100644
index 0000000000..2501f4f6e3
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F01b420e2964928a15f790f9b7c1a0053e7b5f0a5%2Finfo
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000000..89e6c4cae6
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F0bc8b0c38e0790e9ef5c8d512a00b9c4dd048160%2Finfo
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000000..5fc6687589
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F1a6ccc212aa958a0fe76155c2907c889969a7224%2Finfo
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000000..637f9b44da
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2F2d40761dc53e6fa060ac49d88e1452c6751d4b1c%2Finfo
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000000..5f04c62717
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Fb55e5c91d2572d60a8d7e71b3d3003e523127bd4%2Finfo
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000000..a0f1ee088e
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fc%2Ff1246e331cade9341b9e4f311b7a134f99893d21%2Finfo
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000000..34547248b4
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissue%2F2
@@ -0,0 +1,191 @@
+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
new file mode 100644
index 0000000000..683954bdfa
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fissues%3Fpage=1&per_page=20&status=all
@@ -0,0 +1,328 @@
+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
new file mode 100644
index 0000000000..18475372de
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-request%2F5
@@ -0,0 +1,246 @@
+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
new file mode 100644
index 0000000000..f9127f0899
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Fpull-requests%3Fpage=1&per_page=20&status=all
@@ -0,0 +1,1418 @@
+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
new file mode 100644
index 0000000000..416eb8be84
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Faaaa
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..b0bea5052f
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fbbbb
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..378d374b36
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fcccc
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..fa6d75ff1d
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fdddd
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..2f4045095b
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Feeee
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..2261d20aa7
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fffff
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..c18309ee1e
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fgggg
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..92888037b2
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftag%2Fhhhh
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000000..c648a8f338
--- /dev/null
+++ b/services/migrations/testdata/pagure/full_download/unauthorized/GET_%2Fapi%2F0%2Fprotop2g-test-srce%2Ftags
@@ -0,0 +1,23 @@
+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/migrations/update.go b/services/migrations/update.go
index 4a49206f82..4d497c1e2e 100644
--- a/services/migrations/update.go
+++ b/services/migrations/update.go
@@ -6,11 +6,11 @@ package migrations
import (
"context"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/externalaccount"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/structs"
+ "forgejo.org/services/externalaccount"
)
// UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID
diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go
index bc2d6711cf..514b7c3969 100644
--- a/services/mirror/mirror.go
+++ b/services/mirror/mirror.go
@@ -5,14 +5,14 @@ package mirror
import (
"context"
- "fmt"
+ "errors"
- quota_model "code.gitea.io/gitea/models/quota"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
+ quota_model "forgejo.org/models/quota"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
)
// doMirrorSync causes this request to mirror itself
@@ -31,7 +31,7 @@ func doMirrorSync(ctx context.Context, req *SyncRequest) {
}
}
-var errLimit = fmt.Errorf("reached limit")
+var errLimit = errors.New("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 fmt.Errorf("aborted")
+ return errors.New("aborted")
default:
}
diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go
index 085995df4f..c46323f283 100644
--- a/services/mirror/mirror_pull.go
+++ b/services/mirror/mirror_pull.go
@@ -9,21 +9,21 @@ import (
"strings"
"time"
- repo_model "code.gitea.io/gitea/models/repo"
- system_model "code.gitea.io/gitea/models/system"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
- giturl "code.gitea.io/gitea/modules/git/url"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/proxy"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- notify_service "code.gitea.io/gitea/services/notify"
+ repo_model "forgejo.org/models/repo"
+ system_model "forgejo.org/models/system"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
+ giturl "forgejo.org/modules/git/url"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/proxy"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ notify_service "forgejo.org/services/notify"
)
// gitShortEmptySha Git short empty SHA
@@ -244,6 +244,24 @@ 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()
@@ -286,7 +304,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 strings.Contains(stderr, "unable to resolve reference") && strings.Contains(stderr, "reference broken") {
+ if checkRecoverableSyncError(stderr) {
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
@@ -337,6 +355,15 @@ 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)
@@ -346,15 +373,6 @@ 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)
@@ -382,7 +400,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 strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken") {
+ if checkRecoverableSyncError(stderrMessage) {
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
new file mode 100644
index 0000000000..97859be5b0
--- /dev/null
+++ b/services/mirror/mirror_pull_test.go
@@ -0,0 +1,94 @@
+// 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 4b1d7718b6..fdd02dedea 100644
--- a/services/mirror/mirror_push.go
+++ b/services/mirror/mirror_push.go
@@ -13,17 +13,17 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
)
var stripExitStatus = regexp.MustCompile(`exit status \d+ - `)
@@ -33,19 +33,22 @@ var AddPushMirrorRemote = addPushMirrorRemote
func addPushMirrorRemote(ctx context.Context, m *repo_model.PushMirror, addr string) error {
addRemoteAndConfig := func(addr, path string) error {
- 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 --mirror=push %s [repo_path: %s]", m.RemoteName, util.SanitizeCredentialURLs(addr), path))
+ var cmd *git.Command
+ if m.BranchFilter == "" {
+ cmd = git.NewCommand(ctx, "remote", "add", "--mirror").AddDynamicArguments(m.RemoteName, addr)
} else {
- cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=push %s [repo_path: %s]", m.RemoteName, addr, path))
+ cmd = git.NewCommand(ctx, "remote", "add").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))
+ } else {
+ cmd.SetDescription(fmt.Sprintf("remote add %s %s [repo_path: %s]", m.RemoteName, addr, path))
}
if _, _, err := cmd.RunStdString(&git.RunOpts{Dir: path}); err != nil {
return err
}
- 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 {
+ err := addRemotePushRefSpecs(ctx, path, m)
+ if err != nil {
return err
}
return nil
@@ -67,6 +70,49 @@ 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)
@@ -212,7 +258,6 @@ 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
deleted file mode 100644
index 76632b6872..0000000000
--- a/services/mirror/mirror_test.go
+++ /dev/null
@@ -1,66 +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.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/mirror/notifier.go b/services/mirror/notifier.go
index 93d904470d..8f8552f419 100644
--- a/services/mirror/notifier.go
+++ b/services/mirror/notifier.go
@@ -6,10 +6,10 @@ package mirror
import (
"context"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/repository"
- notify_service "code.gitea.io/gitea/services/notify"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/repository"
+ notify_service "forgejo.org/services/notify"
)
func init() {
diff --git a/services/mirror/queue.go b/services/mirror/queue.go
index 0d9a624730..b4869cf8c0 100644
--- a/services/mirror/queue.go
+++ b/services/mirror/queue.go
@@ -4,10 +4,10 @@
package mirror
import (
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
)
var mirrorQueue *queue.WorkerPoolQueue[*SyncRequest]
diff --git a/services/moderation/main_test.go b/services/moderation/main_test.go
new file mode 100644
index 0000000000..3a268260d2
--- /dev/null
+++ b/services/moderation/main_test.go
@@ -0,0 +1,17 @@
+// 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
new file mode 100644
index 0000000000..f329070963
--- /dev/null
+++ b/services/moderation/moderating.go
@@ -0,0 +1,41 @@
+// 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
new file mode 100644
index 0000000000..3d1bb5b32c
--- /dev/null
+++ b/services/moderation/reporting.go
@@ -0,0 +1,170 @@
+// 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
new file mode 100644
index 0000000000..70925bf184
--- /dev/null
+++ b/services/moderation/reporting_test.go
@@ -0,0 +1,97 @@
+// 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 3230a5e5f5..4d88a7ab95 100644
--- a/services/notify/notifier.go
+++ b/services/notify/notifier.go
@@ -6,12 +6,13 @@ package notify
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/repository"
+ actions_model "forgejo.org/models/actions"
+ issues_model "forgejo.org/models/issues"
+ packages_model "forgejo.org/models/packages"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/repository"
)
// Notifier defines an interface to notify receiver
@@ -76,4 +77,6 @@ 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 5ed63646aa..02c18272cb 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -6,13 +6,14 @@ package notify
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
+ actions_model "forgejo.org/models/actions"
+ issues_model "forgejo.org/models/issues"
+ packages_model "forgejo.org/models/packages"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/repository"
)
var notifiers []Notifier
@@ -374,3 +375,15 @@ 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 894d118eac..9c76e5cbd3 100644
--- a/services/notify/null.go
+++ b/services/notify/null.go
@@ -6,12 +6,13 @@ package notify
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/repository"
+ actions_model "forgejo.org/models/actions"
+ issues_model "forgejo.org/models/issues"
+ packages_model "forgejo.org/models/packages"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/repository"
)
// NullNotifier implements a blank notifier
@@ -211,3 +212,7 @@ 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/org/org.go b/services/org/org.go
index dca7794b47..b1bbe43046 100644
--- a/services/org/org.go
+++ b/services/org/org.go
@@ -7,15 +7,15 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- org_model "code.gitea.io/gitea/models/organization"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/util"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ org_model "forgejo.org/models/organization"
+ packages_model "forgejo.org/models/packages"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/util"
+ repo_service "forgejo.org/services/repository"
)
// DeleteOrganization completely and permanently deletes everything of organization.
diff --git a/services/org/org_test.go b/services/org/org_test.go
index 07358438f6..b0f591c745 100644
--- a/services/org/org_test.go
+++ b/services/org/org_test.go
@@ -6,11 +6,11 @@ package org
import (
"testing"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/org/repo.go b/services/org/repo.go
index 78a829ef25..33f55b1191 100644
--- a/services/org/repo.go
+++ b/services/org/repo.go
@@ -7,10 +7,10 @@ import (
"context"
"errors"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
)
// TeamAddRepository adds new repository to team of organization.
diff --git a/services/org/repo_test.go b/services/org/repo_test.go
index 2ddb8f9045..c51cbf4c28 100644
--- a/services/org/repo_test.go
+++ b/services/org/repo_test.go
@@ -6,10 +6,10 @@ package org
import (
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
"github.com/stretchr/testify/require"
)
diff --git a/services/org/team_invite.go b/services/org/team_invite.go
index 3f28044dbf..9c5da25522 100644
--- a/services/org/team_invite.go
+++ b/services/org/team_invite.go
@@ -6,9 +6,9 @@ package org
import (
"context"
- org_model "code.gitea.io/gitea/models/organization"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/services/mailer"
+ org_model "forgejo.org/models/organization"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/services/mailer"
)
// CreateTeamInvite make a persistent invite in db and mail it
diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go
index 92f475bb7b..dd66c7d74e 100644
--- a/services/packages/alpine/repository.go
+++ b/services/packages/alpine/repository.go
@@ -20,14 +20,14 @@ import (
"io"
"strings"
- packages_model "code.gitea.io/gitea/models/packages"
- alpine_model "code.gitea.io/gitea/models/packages/alpine"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/json"
- packages_module "code.gitea.io/gitea/modules/packages"
- alpine_module "code.gitea.io/gitea/modules/packages/alpine"
- "code.gitea.io/gitea/modules/util"
- packages_service "code.gitea.io/gitea/services/packages"
+ packages_model "forgejo.org/models/packages"
+ alpine_model "forgejo.org/models/packages/alpine"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/json"
+ packages_module "forgejo.org/modules/packages"
+ alpine_module "forgejo.org/modules/packages/alpine"
+ "forgejo.org/modules/util"
+ packages_service "forgejo.org/services/packages"
)
const (
@@ -258,7 +258,7 @@ func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *package
privPem, _ := pem.Decode([]byte(priv))
if privPem == nil {
- return fmt.Errorf("failed to decode private key pem")
+ return errors.New("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 f49c435e64..9693f4322e 100644
--- a/services/packages/alt/repository.go
+++ b/services/packages/alt/repository.go
@@ -14,15 +14,15 @@ import (
"path"
"time"
- packages_model "code.gitea.io/gitea/models/packages"
- alt_model "code.gitea.io/gitea/models/packages/alt"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/json"
- packages_module "code.gitea.io/gitea/modules/packages"
- rpm_module "code.gitea.io/gitea/modules/packages/rpm"
- "code.gitea.io/gitea/modules/setting"
- packages_service "code.gitea.io/gitea/services/packages"
+ packages_model "forgejo.org/models/packages"
+ alt_model "forgejo.org/models/packages/alt"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/json"
+ packages_module "forgejo.org/modules/packages"
+ rpm_module "forgejo.org/modules/packages/rpm"
+ "forgejo.org/modules/setting"
+ packages_service "forgejo.org/services/packages"
"github.com/ulikunitz/xz"
)
@@ -714,21 +714,23 @@ func buildRelease(ctx context.Context, pv *packages_model.PackageVersion, pfs []
for architecture := range architectures.Seq() {
version := time.Now().Unix()
label := setting.AppName
- data := fmt.Sprintf(`Archive: Alt Linux Team
+ origin := setting.AppName
+ archive := setting.AppName
+
+ data := fmt.Sprintf(`Archive: %s
Component: classic
Version: %d
-Origin: Alt Linux Team
+Origin: %s
Label: %s
Architecture: %s
NotAutomatic: false
`,
- version, label, architecture)
+ archive, version, origin, 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)
@@ -744,7 +746,7 @@ NotAutomatic: false
data = fmt.Sprintf(`Origin: %s
Label: %s
-Suite: Sisyphus
+Suite: Unknown
Codename: %d
Date: %s
Architectures: %s
diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go
index e681f24561..2a865e6dbd 100644
--- a/services/packages/arch/repository.go
+++ b/services/packages/arch/repository.go
@@ -16,14 +16,14 @@ import (
"sort"
"strings"
- packages_model "code.gitea.io/gitea/models/packages"
- user_model "code.gitea.io/gitea/models/user"
- packages_module "code.gitea.io/gitea/modules/packages"
- arch_module "code.gitea.io/gitea/modules/packages/arch"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/sync"
- "code.gitea.io/gitea/modules/util"
- packages_service "code.gitea.io/gitea/services/packages"
+ packages_model "forgejo.org/models/packages"
+ user_model "forgejo.org/models/user"
+ packages_module "forgejo.org/modules/packages"
+ arch_module "forgejo.org/modules/packages/arch"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/sync"
+ "forgejo.org/modules/util"
+ packages_service "forgejo.org/services/packages"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
diff --git a/services/packages/auth.go b/services/packages/auth.go
index c5bf5af532..205125cf8b 100644
--- a/services/packages/auth.go
+++ b/services/packages/auth.go
@@ -4,15 +4,16 @@
package packages
import (
+ "errors"
"fmt"
"net/http"
"strings"
"time"
- auth_model "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ auth_model "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
"github.com/golang-jwt/jwt/v5"
)
@@ -53,7 +54,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, "", fmt.Errorf("split token failed")
+ return 0, "", errors.New("split token failed")
}
token, err := jwt.ParseWithClaims(parts[1], &packageClaims{}, func(t *jwt.Token) (any, error) {
@@ -68,7 +69,7 @@ func ParseAuthorizationToken(req *http.Request) (int64, auth_model.AccessTokenSc
c, ok := token.Claims.(*packageClaims)
if !token.Valid || !ok {
- return 0, "", fmt.Errorf("invalid token claim")
+ return 0, "", errors.New("invalid token claim")
}
return c.UserID, c.Scope, nil
diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go
index 59823cd3de..9afcd79571 100644
--- a/services/packages/cargo/index.go
+++ b/services/packages/cargo/index.go
@@ -13,17 +13,17 @@ import (
"strconv"
"time"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- cargo_module "code.gitea.io/gitea/modules/packages/cargo"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- repo_service "code.gitea.io/gitea/services/repository"
- files_service "code.gitea.io/gitea/services/repository/files"
+ packages_model "forgejo.org/models/packages"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ cargo_module "forgejo.org/modules/packages/cargo"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ repo_service "forgejo.org/services/repository"
+ files_service "forgejo.org/services/repository/files"
)
const (
diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go
index d84bdf1b03..03ad853216 100644
--- a/services/packages/cleanup/cleanup.go
+++ b/services/packages/cleanup/cleanup.go
@@ -8,20 +8,20 @@ import (
"fmt"
"time"
- "code.gitea.io/gitea/models/db"
- packages_model "code.gitea.io/gitea/models/packages"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- packages_module "code.gitea.io/gitea/modules/packages"
- packages_service "code.gitea.io/gitea/services/packages"
- alpine_service "code.gitea.io/gitea/services/packages/alpine"
- alt_service "code.gitea.io/gitea/services/packages/alt"
- arch_service "code.gitea.io/gitea/services/packages/arch"
- cargo_service "code.gitea.io/gitea/services/packages/cargo"
- container_service "code.gitea.io/gitea/services/packages/container"
- debian_service "code.gitea.io/gitea/services/packages/debian"
- rpm_service "code.gitea.io/gitea/services/packages/rpm"
+ "forgejo.org/models/db"
+ packages_model "forgejo.org/models/packages"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ packages_module "forgejo.org/modules/packages"
+ packages_service "forgejo.org/services/packages"
+ alpine_service "forgejo.org/services/packages/alpine"
+ alt_service "forgejo.org/services/packages/alt"
+ arch_service "forgejo.org/services/packages/arch"
+ cargo_service "forgejo.org/services/packages/cargo"
+ container_service "forgejo.org/services/packages/container"
+ debian_service "forgejo.org/services/packages/debian"
+ rpm_service "forgejo.org/services/packages/rpm"
)
// Task method to execute cleanup rules and cleanup expired package data
@@ -64,13 +64,12 @@ 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 {
+ for _, pv := range pvs[pcr.KeepCount:] {
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)
@@ -122,23 +121,24 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
}
if anyVersionDeleted {
- if pcr.Type == packages_model.TypeDebian {
+ switch pcr.Type {
+ case 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)
}
- } else if pcr.Type == packages_model.TypeAlpine {
+ case 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)
}
- } else if pcr.Type == packages_model.TypeRpm {
+ case 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)
}
- } else if pcr.Type == packages_model.TypeArch {
+ case 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)
}
- } else if pcr.Type == packages_model.TypeAlt {
+ case 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 41dde28248..efa254fc68 100644
--- a/services/packages/cleanup/cleanup_sha256_test.go
+++ b/services/packages/cleanup/cleanup_sha256_test.go
@@ -7,15 +7,15 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- container_module "code.gitea.io/gitea/modules/packages/container"
- "code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/timeutil"
- container_service "code.gitea.io/gitea/services/packages/container"
+ "forgejo.org/models/db"
+ "forgejo.org/models/packages"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ container_module "forgejo.org/modules/packages/container"
+ "forgejo.org/modules/test"
+ "forgejo.org/modules/timeutil"
+ container_service "forgejo.org/services/packages/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -78,7 +78,7 @@ func TestCleanupSHA256(t *testing.T) {
for range expected {
filtered = append(filtered, true)
}
- assert.EqualValues(t, filtered, logFiltered, expected)
+ assert.Equal(t, filtered, logFiltered, expected)
}
ancient := 1 * time.Hour
diff --git a/services/packages/cleanup/main_test.go b/services/packages/cleanup/main_test.go
index ded3d76c83..e9135c147a 100644
--- a/services/packages/cleanup/main_test.go
+++ b/services/packages/cleanup/main_test.go
@@ -6,7 +6,7 @@ package container
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/services/packages/container/blob_uploader.go b/services/packages/container/blob_uploader.go
index bae2e2d6af..ffc47f3853 100644
--- a/services/packages/container/blob_uploader.go
+++ b/services/packages/container/blob_uploader.go
@@ -9,10 +9,10 @@ import (
"io"
"os"
- packages_model "code.gitea.io/gitea/models/packages"
- packages_module "code.gitea.io/gitea/modules/packages"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ packages_model "forgejo.org/models/packages"
+ packages_module "forgejo.org/modules/packages"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
)
var (
@@ -92,7 +92,7 @@ func (u *BlobUploader) Append(ctx context.Context, r io.Reader) error {
u.BytesReceived += n
- u.HashStateBytes, err = u.MultiHasher.MarshalBinary()
+ u.HashStateBytes, err = u.MarshalBinary()
if err != nil {
return err
}
diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go
index b5563c688f..a4ae7118f5 100644
--- a/services/packages/container/cleanup.go
+++ b/services/packages/container/cleanup.go
@@ -7,11 +7,11 @@ import (
"context"
"time"
- packages_model "code.gitea.io/gitea/models/packages"
- container_model "code.gitea.io/gitea/models/packages/container"
- "code.gitea.io/gitea/modules/optional"
- container_module "code.gitea.io/gitea/modules/packages/container"
- packages_service "code.gitea.io/gitea/services/packages"
+ packages_model "forgejo.org/models/packages"
+ container_model "forgejo.org/models/packages/container"
+ "forgejo.org/modules/optional"
+ container_module "forgejo.org/modules/packages/container"
+ packages_service "forgejo.org/services/packages"
digest "github.com/opencontainers/go-digest"
)
diff --git a/services/packages/container/cleanup_sha256.go b/services/packages/container/cleanup_sha256.go
index 16afc74b18..5d0d02c22d 100644
--- a/services/packages/container/cleanup_sha256.go
+++ b/services/packages/container/cleanup_sha256.go
@@ -8,12 +8,12 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- container_module "code.gitea.io/gitea/modules/packages/container"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/models/db"
+ "forgejo.org/models/packages"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ container_module "forgejo.org/modules/packages/container"
+ "forgejo.org/modules/timeutil"
)
var (
diff --git a/services/packages/container/common.go b/services/packages/container/common.go
index 5a14ed5b7a..758ef51721 100644
--- a/services/packages/container/common.go
+++ b/services/packages/container/common.go
@@ -7,9 +7,9 @@ import (
"context"
"strings"
- packages_model "code.gitea.io/gitea/models/packages"
- user_model "code.gitea.io/gitea/models/user"
- container_module "code.gitea.io/gitea/modules/packages/container"
+ packages_model "forgejo.org/models/packages"
+ user_model "forgejo.org/models/user"
+ container_module "forgejo.org/modules/packages/container"
)
// UpdateRepositoryNames updates the repository name property for all packages of the specific owner
diff --git a/services/packages/debian/repository.go b/services/packages/debian/repository.go
index e400f1e924..a8a401662e 100644
--- a/services/packages/debian/repository.go
+++ b/services/packages/debian/repository.go
@@ -14,14 +14,14 @@ import (
"strings"
"time"
- packages_model "code.gitea.io/gitea/models/packages"
- debian_model "code.gitea.io/gitea/models/packages/debian"
- user_model "code.gitea.io/gitea/models/user"
- packages_module "code.gitea.io/gitea/modules/packages"
- debian_module "code.gitea.io/gitea/modules/packages/debian"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- packages_service "code.gitea.io/gitea/services/packages"
+ packages_model "forgejo.org/models/packages"
+ debian_model "forgejo.org/models/packages/debian"
+ user_model "forgejo.org/models/user"
+ packages_module "forgejo.org/modules/packages"
+ debian_module "forgejo.org/modules/packages/debian"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ packages_service "forgejo.org/services/packages"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
diff --git a/services/packages/package_update.go b/services/packages/package_update.go
index 4a22ee7a62..7fa938e260 100644
--- a/services/packages/package_update.go
+++ b/services/packages/package_update.go
@@ -7,13 +7,13 @@ import (
"context"
"fmt"
- org_model "code.gitea.io/gitea/models/organization"
- packages_model "code.gitea.io/gitea/models/packages"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/util"
+ org_model "forgejo.org/models/organization"
+ packages_model "forgejo.org/models/packages"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/util"
)
func LinkToRepository(ctx context.Context, pkg *packages_model.Package, repo *repo_model.Repository, doer *user_model.User) error {
diff --git a/services/packages/packages.go b/services/packages/packages.go
index bf89b6ad35..418ceab798 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -12,17 +12,17 @@ import (
"net/url"
"strings"
- "code.gitea.io/gitea/models/db"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- packages_module "code.gitea.io/gitea/modules/packages"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ packages_model "forgejo.org/models/packages"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ packages_module "forgejo.org/modules/packages"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ notify_service "forgejo.org/services/notify"
)
var (
@@ -127,12 +127,12 @@ func createPackageAndVersion(ctx context.Context, pvci *PackageCreationInfo, all
OwnerID: pvci.Owner.ID,
Type: pvci.PackageType,
Name: pvci.Name,
- LowerName: strings.ToLower(pvci.Name),
+ LowerName: packages_model.ResolvePackageName(pvci.Name, pvci.PackageType),
SemverCompatible: pvci.SemverCompatible,
}
var err error
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
- if err == packages_model.ErrDuplicatePackage {
+ if errors.Is(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)
+ return addFileToPackageVersionUnchecked(ctx, pv, pfci, pvi.PackageType)
}
-func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo) (*packages_model.PackageFile, *packages_model.PackageBlob, bool, error) {
+func addFileToPackageVersionUnchecked(ctx context.Context, pv *packages_model.PackageVersion, pfci *PackageFileCreationInfo, packageType packages_model.Type) (*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: strings.ToLower(pfci.Filename),
+ LowerName: packages_model.ResolvePackageName(pfci.Filename, packageType),
CompositeKey: pfci.CompositeKey,
IsLead: pfci.IsLead,
}
diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go
index 705876e5c0..26f34be2bc 100644
--- a/services/packages/rpm/repository.go
+++ b/services/packages/rpm/repository.go
@@ -16,20 +16,20 @@ import (
"strings"
"time"
- packages_model "code.gitea.io/gitea/models/packages"
- rpm_model "code.gitea.io/gitea/models/packages/rpm"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- packages_module "code.gitea.io/gitea/modules/packages"
- rpm_module "code.gitea.io/gitea/modules/packages/rpm"
- "code.gitea.io/gitea/modules/util"
- packages_service "code.gitea.io/gitea/services/packages"
+ packages_model "forgejo.org/models/packages"
+ rpm_model "forgejo.org/models/packages/rpm"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ packages_module "forgejo.org/modules/packages"
+ rpm_module "forgejo.org/modules/packages/rpm"
+ "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,7 +410,6 @@ 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,
@@ -439,7 +438,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, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
+ 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),
},
Format: Format{
License: pd.VersionMetadata.License,
diff --git a/services/pull/check.go b/services/pull/check.go
index 2d91ed07f5..c31d107605 100644
--- a/services/pull/check.go
+++ b/services/pull/check.go
@@ -11,23 +11,24 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/timeutil"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/queue"
+ "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
@@ -170,7 +171,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) {
+func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) bool {
// 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
@@ -184,12 +185,15 @@ func checkAndUpdateStatus(ctx context.Context, pr *issues_model.PullRequest) {
if has {
log.Trace("Not updating status for %-v as it is due to be rechecked", pr)
- return
+ return false
}
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
@@ -339,15 +343,22 @@ func handler(items ...string) []string {
}
func testPR(id int64) {
- 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()
+ 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))
+
pr, err := issues_model.GetPullRequestByID(ctx, id)
if err != nil {
log.Error("Unable to GetPullRequestByID[%d] for testPR: %v", id, err)
- return
+ return nil, false
}
log.Trace("Testing %-v", pr)
@@ -357,12 +368,12 @@ func testPR(id int64) {
if pr.HasMerged {
log.Trace("%-v is already merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
- return
+ return nil, false
}
if manuallyMerged(ctx, pr) {
log.Trace("%-v is manually merged (status: %s, merge commit: %s)", pr, pr.Status, pr.MergedCommitID)
- return
+ return nil, false
}
if err := TestPatch(pr); err != nil {
@@ -371,9 +382,10 @@ func testPR(id int64) {
if err := pr.UpdateCols(ctx, "status"); err != nil {
log.Error("update pr [%-v] status to PullRequestStatusError failed: %v", pr, err)
}
- return
+ return nil, false
}
- checkAndUpdateStatus(ctx, pr)
+
+ return pr, checkAndUpdateStatus(ctx, pr)
}
// CheckPRsForBaseBranch check all pulls with baseBrannch
@@ -392,10 +404,14 @@ 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 fmt.Errorf("unable to create pr_patch_checker queue")
+ return errors.New("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 dfb8ff708b..9b7e1660bc 100644
--- a/services/pull/check_test.go
+++ b/services/pull/check_test.go
@@ -9,11 +9,11 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -52,7 +52,7 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
select {
case id := <-idChan:
- assert.EqualValues(t, pr.ID, id)
+ assert.Equal(t, pr.ID, id)
case <-time.After(time.Second):
assert.FailNow(t, "Timeout: nothing was added to pullRequestQueue")
}
diff --git a/services/pull/comment.go b/services/pull/comment.go
index 53587d4f54..25542daa9f 100644
--- a/services/pull/comment.go
+++ b/services/pull/comment.go
@@ -6,11 +6,11 @@ package pull
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/json"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/json"
)
// getCommitIDsFromRepo get commit IDs from repo in between oldCommitID and newCommitID
diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go
index 2c77d9cf4e..0a95ea1152 100644
--- a/services/pull/commit_status.go
+++ b/services/pull/commit_status.go
@@ -9,13 +9,12 @@ import (
"errors"
"fmt"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/structs"
"github.com/gobwas/glob"
)
@@ -105,7 +104,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 && !git.IsReferenceExist(ctx, headGitRepo.Path, pr.GetGitRefName()) {
+ if pr.Flow == issues_model.PullRequestFlowAGit && !headGitRepo.IsReferenceExist(pr.GetGitRefName()) {
return "", errors.New("head branch does not exist, can not merge")
}
diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go
index 592acdd55c..593c88fb19 100644
--- a/services/pull/commit_status_test.go
+++ b/services/pull/commit_status_test.go
@@ -7,8 +7,8 @@ package pull
import (
"testing"
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/modules/structs"
+ git_model "forgejo.org/models/git"
+ "forgejo.org/modules/structs"
"github.com/stretchr/testify/assert"
)
diff --git a/services/pull/edits.go b/services/pull/edits.go
index c7550dcb07..dbf08e6851 100644
--- a/services/pull/edits.go
+++ b/services/pull/edits.go
@@ -8,10 +8,10 @@ import (
"context"
"errors"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- unit_model "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ unit_model "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
)
var ErrUserHasNoPermissionForAction = errors.New("user not allowed to do this action")
diff --git a/services/pull/lfs.go b/services/pull/lfs.go
index ed03583d4f..8d9f401641 100644
--- a/services/pull/lfs.go
+++ b/services/pull/lfs.go
@@ -11,12 +11,12 @@ import (
"strconv"
"sync"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/modules/git/pipeline"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/git/pipeline"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
)
// LFSPush pushes lfs objects referred to in new commits in the head repository from the base repository
diff --git a/services/pull/main_test.go b/services/pull/main_test.go
index 4bcb50fb96..5262b5be50 100644
--- a/services/pull/main_test.go
+++ b/services/pull/main_test.go
@@ -7,10 +7,10 @@ package pull
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
)
func TestMain(m *testing.M) {
diff --git a/services/pull/merge.go b/services/pull/merge.go
index a1585e64ab..a2542176f0 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -6,6 +6,7 @@ package pull
import (
"context"
+ "errors"
"fmt"
"net/url"
"os"
@@ -13,26 +14,51 @@ import (
"regexp"
"strconv"
"strings"
+ "unicode"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/references"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- issue_service "code.gitea.io/gitea/services/issue"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/references"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ issue_service "forgejo.org/services/issue"
+ 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 {
@@ -77,6 +103,13 @@ 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
@@ -168,10 +201,68 @@ 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 {
- if err := pr.LoadBaseRepo(ctx); err != nil {
+ 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 {
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 {
@@ -179,9 +270,6 @@ 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)
@@ -518,13 +606,13 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if len(commitID) != objectFormat.FullLength() {
- return fmt.Errorf("Wrong commit ID")
+ return errors.New("Wrong commit ID")
}
commit, err := baseGitRepo.GetCommit(commitID)
if err != nil {
if git.IsErrNotExist(err) {
- return fmt.Errorf("Wrong commit ID")
+ return errors.New("Wrong commit ID")
}
return err
}
@@ -535,7 +623,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use
return err
}
if !ok {
- return fmt.Errorf("Wrong commit ID")
+ return errors.New("Wrong commit ID")
}
pr.MergedCommitID = commitID
@@ -548,7 +636,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 fmt.Errorf("SetMerged failed")
+ return errors.New("SetMerged failed")
}
return nil
}); err != nil {
diff --git a/services/pull/merge_ff_only.go b/services/pull/merge_ff_only.go
index f57c732104..1e1c337889 100644
--- a/services/pull/merge_ff_only.go
+++ b/services/pull/merge_ff_only.go
@@ -4,9 +4,9 @@
package pull
import (
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
)
// doMergeStyleFastForwardOnly merges the tracking into the current HEAD - which is assumed to be staging branch (equal to the pr.BaseBranch)
diff --git a/services/pull/merge_merge.go b/services/pull/merge_merge.go
index bf56c071db..713e1f175d 100644
--- a/services/pull/merge_merge.go
+++ b/services/pull/merge_merge.go
@@ -4,9 +4,9 @@
package pull
import (
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
)
// doMergeStyleMerge merges the tracking branch into the current HEAD - which is assumed to be the staging branch (equal to the pr.BaseBranch)
diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go
index 88f6c037eb..4598d57b7a 100644
--- a/services/pull/merge_prepare.go
+++ b/services/pull/merge_prepare.go
@@ -14,13 +14,13 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "forgejo.org/models"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ asymkey_service "forgejo.org/services/asymkey"
)
type mergeContext struct {
@@ -236,10 +236,77 @@ 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 {
- // Checkout head branch
- if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch).
+ // Create staging branch
+ if err := git.NewCommand(ctx, "branch").AddDynamicArguments(stagingBranch, trackingBranch).
Run(ctx.RunOpts()); err != nil {
- 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())
+ 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).
+ 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()
diff --git a/services/pull/merge_rebase.go b/services/pull/merge_rebase.go
index ecf376220e..088934cdfb 100644
--- a/services/pull/merge_rebase.go
+++ b/services/pull/merge_rebase.go
@@ -7,10 +7,10 @@ import (
"fmt"
"strings"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
)
// getRebaseAmendMessage composes the message to amend commits in rebase merge of a pull request.
diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go
index 6dda46eaec..f655224c5e 100644
--- a/services/pull/merge_squash.go
+++ b/services/pull/merge_squash.go
@@ -5,15 +5,14 @@ package pull
import (
"fmt"
- "strings"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
)
// doMergeStyleSquash gets a commit author signature for squash commits
@@ -67,10 +66,8 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() {
// add trailer
- 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())
+ 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
}
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 6df6f55d46..aa033b8cdb 100644
--- a/services/pull/merge_test.go
+++ b/services/pull/merge_test.go
@@ -4,9 +4,22 @@
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) {
@@ -65,3 +78,102 @@ 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 e90b4bdbbe..89581c916b 100644
--- a/services/pull/patch.go
+++ b/services/pull/patch.go
@@ -5,7 +5,7 @@
package pull
import (
- "bufio"
+ "bytes"
"context"
"fmt"
"io"
@@ -13,18 +13,15 @@ import (
"path/filepath"
"strings"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/util"
"github.com/gobwas/glob"
)
@@ -49,66 +46,162 @@ 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()
- 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)
+ testPatchCtx, err := testPatch(ctx, pr)
+ testPatchCtx.close()
+ return err
}
-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()
+type testPatchContext struct {
+ headRev string
+ headIsCommitID bool
+ baseRev string
+ env []string
+ gitRepo *git.Repository
+ close func()
+}
- // 1. update merge base
- 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 = gitRepo.GetRefCommitID(git.BranchPrefix + "base")
- if err2 != nil {
- return fmt.Errorf("GetMergeBase: %v and can't find commit ID for base: %w", err, err2)
+// 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
}
- }
- pr.MergeBase = strings.TrimSpace(pr.MergeBase)
- 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 pr.HeadCommitID == pr.MergeBase {
- pr.Status = issues_model.PullRequestStatusAncestor
+ t.headRev = pr.GetGitRefName()
return nil
}
- // 2. Check for conflicts
- if conflicts, err := checkConflicts(ctx, pr, gitRepo, prCtx.tmpBasePath); err != nil || conflicts || pr.Status == issues_model.PullRequestStatusEmpty {
+ // 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())
+ if err != nil {
+ return 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})
+ if err != nil {
+ var err2 error
+ pr.MergeBase, err2 = testPatchCtx.gitRepo.GetRefCommitID(testPatchCtx.baseRev)
+ if err2 != nil {
+ return testPatchCtx, 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 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
+ }
+
+ // 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
+ }
+
// 3. Check for protected files changes
- if err = checkPullFilesProtection(ctx, pr, gitRepo); err != nil {
- return fmt.Errorf("pr.CheckPullFilesProtection(): %v", err)
+ if err = checkPullFilesProtection(ctx, pr, testPatchCtx); err != nil {
+ return testPatchCtx, fmt.Errorf("checkPullFilesProtection: %v", err)
}
if len(pr.ChangedProtectedFiles) > 0 {
@@ -117,7 +210,7 @@ func testPatch(ctx context.Context, prCtx *prContext, pr *issues_model.PullReque
pr.Status = issues_model.PullRequestStatusMergeable
- return nil
+ return testPatchCtx, nil
}
type errMergeConflict struct {
@@ -236,12 +329,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, gitPath string, gitRepo *git.Repository, base, ours, theirs, description string) (bool, []string, error) {
+func AttemptThreeWayMerge(ctx context.Context, 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: gitPath}); err != nil {
+ if _, _, err := git.NewCommand(ctx, "read-tree", "-m").AddDynamicArguments(base, ours, theirs).RunStdString(&git.RunOpts{Dir: gitRepo.Path}); 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)
}
@@ -251,7 +344,7 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo
// 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, gitPath, unmerged)
+ go unmergedFiles(ctx, gitRepo.Path, unmerged)
defer func() {
cancel()
@@ -274,7 +367,7 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo
}
// OK now we have the unmerged file triplet attempt to merge it
- if err := attemptMerge(ctx, file, gitPath, &filesToRemove, &filesToAdd); err != nil {
+ if err := attemptMerge(ctx, file, gitRepo.Path, &filesToRemove, &filesToAdd); err != nil {
if conflictErr, ok := err.(*errMergeConflict); ok {
log.Trace("Conflict: %s in %s", conflictErr.filename, description)
conflict = true
@@ -299,14 +392,82 @@ func AttemptThreeWayMerge(ctx context.Context, gitPath string, gitRepo *git.Repo
return conflict, conflictedFiles, nil
}
-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
+// 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.
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,
- tmpBasePath, gitRepo, pr.MergeBase, "base", "tracking", description)
+ conflict, conflictFiles, err := AttemptThreeWayMerge(ctx, testPatchCtx.gitRepo, pr.MergeBase, testPatchCtx.baseRev, testPatchCtx.headRev, description)
if err != nil {
return false, err
}
@@ -315,13 +476,13 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *
// 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: tmpBasePath})
+ treeHash, _, err = git.NewCommand(ctx, "write-tree").RunStdString(&git.RunOpts{Dir: testPatchCtx.gitRepo.Path})
if err != nil {
- lsfiles, _, _ := git.NewCommand(ctx, "ls-files", "-u").RunStdString(&git.RunOpts{Dir: tmpBasePath})
+ lsfiles, _, _ := git.NewCommand(ctx, "ls-files", "-u").RunStdString(&git.RunOpts{Dir: testPatchCtx.gitRepo.Path})
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 := gitRepo.GetTree("base")
+ baseTree, err := testPatchCtx.gitRepo.GetTree(testPatchCtx.baseRev)
if err != nil {
return false, err
}
@@ -335,171 +496,11 @@ func checkConflicts(ctx context.Context, pr *issues_model.PullRequest, gitRepo *
return false, nil
}
- // 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
+ pr.Status = issues_model.PullRequestStatusConflict
+ pr.ConflictedFiles = conflictFiles
- 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
+ log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
+ return true, nil
}
// CheckFileProtection check file Protection
@@ -558,7 +559,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, gitRepo *git.Repository) error {
+func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest, testPatchCtx *testPatchContext) error {
if pr.Status == issues_model.PullRequestStatusEmpty {
pr.ChangedProtectedFiles = nil
return nil
@@ -574,7 +575,7 @@ func checkPullFilesProtection(ctx context.Context, pr *issues_model.PullRequest,
return nil
}
- pr.ChangedProtectedFiles, err = CheckFileProtection(gitRepo, pr.MergeBase, "tracking", pb.GetProtectedFilePatterns(), 10, os.Environ())
+ pr.ChangedProtectedFiles, err = CheckFileProtection(testPatchCtx.gitRepo, pr.MergeBase, testPatchCtx.headRev, pb.GetProtectedFilePatterns(), 10, testPatchCtx.env)
if err != nil && !models.IsErrFilePathProtected(err) {
return err
}
diff --git a/services/pull/patch_test.go b/services/pull/patch_test.go
new file mode 100644
index 0000000000..bcab19fc58
--- /dev/null
+++ b/services/pull/patch_test.go
@@ -0,0 +1,62 @@
+// 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/patch_unmerged.go b/services/pull/patch_unmerged.go
index c60c48d923..caa0318c48 100644
--- a/services/pull/patch_unmerged.go
+++ b/services/pull/patch_unmerged.go
@@ -13,8 +13,8 @@ import (
"strconv"
"strings"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
)
// lsFileLine is a Quadruplet struct (+error) representing a partially parsed line from ls-files
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 6af7d8ba0c..c1fe6c6b55 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -4,36 +4,33 @@
package pull
import (
- "bytes"
"context"
"fmt"
"io"
- "os"
"regexp"
"strings"
"time"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/sync"
- "code.gitea.io/gitea/modules/util"
- gitea_context "code.gitea.io/gitea/services/context"
- issue_service "code.gitea.io/gitea/services/issue"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/sync"
+ gitea_context "forgejo.org/services/context"
+ issue_service "forgejo.org/services/issue"
+ notify_service "forgejo.org/services/notify"
)
// TODO: use clustered lock (unique queue? or *abuse* cache)
@@ -46,22 +43,15 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
return user_model.ErrBlockedByUser
}
- prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
+ testPatchCtx, err := testPatch(ctx, pr)
+ defer testPatchCtx.close()
if err != nil {
- 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
+ return fmt.Errorf("testPatch: %w", err)
}
- divergence, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
+ divergence, err := git.GetDivergingCommits(ctx, testPatchCtx.gitRepo.Path, testPatchCtx.baseRev, testPatchCtx.headRev, testPatchCtx.env)
if err != nil {
- return err
+ return fmt.Errorf("GetDivergingCommits: %w", err)
}
pr.CommitsAhead = divergence.Ahead
pr.CommitsBehind = divergence.Behind
@@ -391,98 +381,51 @@ 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() {
- changed, err := checkIfPRContentChanged(ctx, pr, oldCommitID, newCommitID)
+ 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 err != nil {
- log.Error("checkIfPRContentChanged: %v", err)
+ log.Error("GetFirstMatchProtectedBranchRule: %v", err)
}
- if changed {
- if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
- log.Error("MarkReviewsAsStale: %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)
+ }
- 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)
+ divergence, err := git.GetDivergingCommits(ctx, testPatchCtx.gitRepo.Path, testPatchCtx.baseRev, testPatchCtx.headRev, testPatchCtx.env)
+ if err != nil {
+ log.Error("GetDivergingCommits: %v", err)
+ } else {
+ err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
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)
- }
+ log.Error("UpdateCommitDivergence: %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)
- }
-
- cmd := git.NewCommand(ctx, "diff", "--name-only", "-z").AddDynamicArguments(newCommitID, oldCommitID, base)
- stdoutReader, stdoutWriter, err := os.Pipe()
- if err != nil {
- 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
// corresponding branches of base repository.
// FIXME: Only push branches that are actually updates?
diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go
index c51619e7f6..99607c5b35 100644
--- a/services/pull/pull_test.go
+++ b/services/pull/pull_test.go
@@ -7,13 +7,13 @@ package pull
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -92,3 +92,39 @@ 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 927c43150b..b0ab700fa6 100644
--- a/services/pull/review.go
+++ b/services/pull/review.go
@@ -6,26 +6,23 @@ package pull
import (
"context"
+ "errors"
"fmt"
"io"
- "regexp"
- "strings"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ 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{}
@@ -47,8 +44,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, repo.Path, c.TreePath, uint(c.UnsignedLine()))
- if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
+ commit, err := repo.LineBlame(branch, c.TreePath, c.UnsignedLine())
+ if err != nil && (errors.Is(err, git.ErrBlameFileDoesNotExist) || errors.Is(err, git.ErrBlameFileNotEnoughLines)) {
c.Invalidated = true
return issues_model.UpdateCommentInvalidate(ctx, c)
}
@@ -229,10 +226,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, gitRepo.Path, treePath, uint(line))
+ commit, err := gitRepo.LineBlame(head, treePath, uint64(line))
if err == nil {
commitID = commit.ID.String()
- } else if !(strings.Contains(err.Error(), "exit status 128 - fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
+ } else if !errors.Is(err, git.ErrBlameFileDoesNotExist) && !errors.Is(err, git.ErrBlameFileNotEnoughLines) {
return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitRefName(), gitRepo.Path, treePath, line, err)
}
}
@@ -301,10 +298,16 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
if headCommitID == commitID {
stale = false
} else {
- stale, err = checkIfPRContentChanged(ctx, pr, commitID, headCommitID)
+ testPatchCtx, err := getTestPatchCtx(ctx, pr, true)
+ defer testPatchCtx.close()
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)
+ }
}
}
@@ -387,7 +390,7 @@ func DismissReview(ctx context.Context, reviewID, repoID int64, message string,
}
if review.Type != issues_model.ReviewTypeApprove && review.Type != issues_model.ReviewTypeReject {
- return nil, fmt.Errorf("not need to dismiss this review because it's type is not Approve or change request")
+ return nil, errors.New("not need to dismiss this review because it's type is not Approve or change request")
}
// load data for notify
@@ -397,7 +400,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, fmt.Errorf("reviews's repository is not the same as the one we expect")
+ return nil, errors.New("reviews's repository is not the same as the one we expect")
}
issue := review.Issue
diff --git a/services/pull/review_test.go b/services/pull/review_test.go
index 4cb3ad007c..0d9fe7f902 100644
--- a/services/pull/review_test.go
+++ b/services/pull/review_test.go
@@ -6,11 +6,11 @@ package pull_test
import (
"testing"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- pull_service "code.gitea.io/gitea/services/pull"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ pull_service "forgejo.org/services/pull"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/pull/temp_repo.go b/services/pull/temp_repo.go
index 36bdbde55c..76ae0df018 100644
--- a/services/pull/temp_repo.go
+++ b/services/pull/temp_repo.go
@@ -11,12 +11,12 @@ import (
"path/filepath"
"strings"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
)
// Temporary repos created here use standard branch names to help simplify
@@ -103,11 +103,7 @@ func createTemporaryRepoForPR(ctx context.Context, pr *issues_model.PullRequest)
remoteRepoName := "head_repo"
baseBranch := "base"
- 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")
- }
+ fetchArgs := git.TrustedCmdArgs{"--no-tags", "--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 dbc1b711e2..563c11fff3 100644
--- a/services/pull/update.go
+++ b/services/pull/update.go
@@ -5,24 +5,25 @@ package pull
import (
"context"
+ "errors"
"fmt"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/repository"
)
// Update updates pull request with base branch.
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 fmt.Errorf("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")
}
pullWorkingPool.CheckIn(fmt.Sprint(pr.ID))
@@ -175,6 +176,6 @@ func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.Diver
}
defer cancel()
- diff, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
+ diff, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch, nil)
return &diff, err
}
diff --git a/services/pull/update_rebase.go b/services/pull/update_rebase.go
index 3e2a7be132..b12613985a 100644
--- a/services/pull/update_rebase.go
+++ b/services/pull/update_rebase.go
@@ -8,13 +8,13 @@ import (
"fmt"
"strings"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
)
// updateHeadByRebaseOnToBase handles updating a PR's head branch by rebasing it on the PR current base branch
diff --git a/services/redirect/main_test.go b/services/redirect/main_test.go
new file mode 100644
index 0000000000..7363791caa
--- /dev/null
+++ b/services/redirect/main_test.go
@@ -0,0 +1,18 @@
+// 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
new file mode 100644
index 0000000000..7070ab4e2f
--- /dev/null
+++ b/services/redirect/repo.go
@@ -0,0 +1,37 @@
+// 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
new file mode 100644
index 0000000000..cad8414035
--- /dev/null
+++ b/services/redirect/repo_test.go
@@ -0,0 +1,55 @@
+// 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
new file mode 100644
index 0000000000..2b1d38bbc0
--- /dev/null
+++ b/services/redirect/user.go
@@ -0,0 +1,30 @@
+// 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
new file mode 100644
index 0000000000..d1ddcc2ebf
--- /dev/null
+++ b/services/redirect/user_test.go
@@ -0,0 +1,65 @@
+// 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 b52e4b124e..90eb1320ed 100644
--- a/services/release/release.go
+++ b/services/release/release.go
@@ -9,22 +9,22 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/attachment"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/attachment"
+ notify_service "forgejo.org/services/notify"
)
type AttachmentChange struct {
@@ -161,17 +161,17 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, msg string,
for _, attachmentChange := range attachmentChanges {
if attachmentChange.Action != "add" {
- return fmt.Errorf("can only create new attachments when creating release")
+ return errors.New("can only create new attachments when creating release")
}
switch attachmentChange.Type {
case "attachment":
if attachmentChange.UUID == "" {
- return fmt.Errorf("new attachment should have a uuid")
+ return errors.New("new attachment should have a uuid")
}
addAttachmentUUIDs.Add(attachmentChange.UUID)
case "external":
if attachmentChange.Name == "" || attachmentChange.ExternalURL == "" {
- return fmt.Errorf("new external attachment should have a name and external url")
+ return errors.New("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 fmt.Errorf("missing attachment type")
+ return errors.New("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 fmt.Errorf("new external attachment should have a name and external url")
+ return errors.New("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 fmt.Errorf("missing attachment type")
+ return errors.New("missing attachment type")
}
return fmt.Errorf("unknown attachment type: %q", attachmentChange.Type)
}
case "delete":
if attachmentChange.UUID == "" {
- return fmt.Errorf("attachment deletion should have a uuid")
+ return errors.New("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 fmt.Errorf("missing attachment action")
+ return errors.New("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 5a22c473cf..f03b4d42b8 100644
--- a/services/release/release_test.go
+++ b/services/release/release_test.go
@@ -6,18 +6,18 @@ package release
import (
"strings"
"testing"
- "time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/services/attachment"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/test"
+ "forgejo.org/services/attachment"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.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)
+ 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)
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.EqualValues(t, "test", release.Attachments[0].Name)
- assert.EqualValues(t, "https://forgejo.org/", release.Attachments[0].ExternalURL)
+ assert.Equal(t, "test", release.Attachments[0].Name)
+ assert.Equal(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
- time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
+ test.SleepTillNextSecond()
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
- time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
+ test.SleepTillNextSecond()
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
- time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
+ test.SleepTillNextSecond()
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.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)
+ 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)
// 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.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)
+ 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)
// 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.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)
+ 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)
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.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)
+ 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)
}
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
- time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
+ test.SleepTillNextSecond()
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
- time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
+ test.SleepTillNextSecond()
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
- time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
+ test.SleepTillNextSecond()
release.Title = "Changed title"
release.Note = "Changed note"
_, err = createTag(db.DefaultContext, gitRepo, release, "")
diff --git a/services/release/tag.go b/services/release/tag.go
index dae2b70f76..e1608d1897 100644
--- a/services/release/tag.go
+++ b/services/release/tag.go
@@ -8,12 +8,12 @@ import (
"errors"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/queue"
- repo_module "code.gitea.io/gitea/modules/repository"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/queue"
+ repo_module "forgejo.org/modules/repository"
"xorm.io/builder"
)
diff --git a/services/remote/promote.go b/services/remote/promote.go
index eb41ace462..f37d00168c 100644
--- a/services/remote/promote.go
+++ b/services/remote/promote.go
@@ -6,12 +6,12 @@ package remote
import (
"context"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/services/auth/source/oauth2"
- remote_source "code.gitea.io/gitea/services/auth/source/remote"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ "forgejo.org/services/auth/source/oauth2"
+ remote_source "forgejo.org/services/auth/source/remote"
)
type Reason int
@@ -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.ERROR, ReasonLoginNameNotExists, "no user with LoginType UserTypeRemoteUser and LoginName '%s'", loginName), nil
+ return nil, NewReason(log.DEBUG, ReasonLoginNameNotExists, "no user with LoginType UserTypeRemoteUser and LoginName '%s'", loginName), nil
}
reason := ReasonNoSource
diff --git a/services/repository/adopt.go b/services/repository/adopt.go
index 3d6fe71a09..3651b018e6 100644
--- a/services/repository/adopt.go
+++ b/services/repository/adopt.go
@@ -11,19 +11,19 @@ import (
"path/filepath"
"strings"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ notify_service "forgejo.org/services/notify"
"github.com/gobwas/glob"
)
diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go
index 71fb1fc885..a66b4c5ac0 100644
--- a/services/repository/adopt_test.go
+++ b/services/repository/adopt_test.go
@@ -8,11 +8,11 @@ import (
"path"
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go
index 279067c002..cffb07210f 100644
--- a/services/repository/archiver/archiver.go
+++ b/services/repository/archiver/archiver.go
@@ -12,16 +12,16 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
)
// ArchiveRequest defines the parameters of an archive request, which notably
diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go
index e7a2422e55..00d82267c9 100644
--- a/services/repository/archiver/archiver_test.go
+++ b/services/repository/archiver/archiver_test.go
@@ -7,13 +7,13 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/services/contexttest"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/git"
+ "forgejo.org/services/contexttest"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.EqualValues(t, firstCommit+".zip", bogusReq.GetArchiveName())
+ assert.Equal(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.EqualValues(t, "master.zip", bogusReq.GetArchiveName())
+ assert.Equal(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.EqualValues(t, "test-archive.zip", bogusReq.GetArchiveName())
+ assert.Equal(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.go b/services/repository/avatar.go
index 32940a7aa3..a1cd3228df 100644
--- a/services/repository/avatar.go
+++ b/services/repository/avatar.go
@@ -9,11 +9,11 @@ import (
"io"
"strconv"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/avatar"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/storage"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/avatar"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/storage"
)
// UploadAvatar saves custom avatar for repository.
diff --git a/services/repository/avatar_test.go b/services/repository/avatar_test.go
index b3c498dfc8..6f28113286 100644
--- a/services/repository/avatar_test.go
+++ b/services/repository/avatar_test.go
@@ -9,10 +9,10 @@ import (
"image/png"
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/avatar"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/avatar"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -60,7 +60,7 @@ func TestDeleteAvatar(t *testing.T) {
err = DeleteAvatar(db.DefaultContext, repo)
require.NoError(t, err)
- assert.Equal(t, "", repo.Avatar)
+ assert.Empty(t, repo.Avatar)
}
func TestTemplateGenerateAvatar(t *testing.T) {
diff --git a/services/repository/branch.go b/services/repository/branch.go
index 8e1a6cd27f..bc739825a5 100644
--- a/services/repository/branch.go
+++ b/services/repository/branch.go
@@ -9,28 +9,29 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/queue"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- notify_service "code.gitea.io/gitea/services/notify"
- pull_service "code.gitea.io/gitea/services/pull"
- files_service "code.gitea.io/gitea/services/repository/files"
+ "forgejo.org/models"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/queue"
+ repo_module "forgejo.org/modules/repository"
+ "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"
"xorm.io/builder"
)
@@ -250,7 +251,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
// For other batches, it will hit optimization 4.
if len(branchNames) != len(commitIDs) {
- return fmt.Errorf("branchNames and commitIDs length not match")
+ return errors.New("branchNames and commitIDs length not match")
}
return db.WithTx(ctx, func(ctx context.Context) error {
@@ -377,7 +378,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_model.CancelPreviousJobs(
+ if err := actions_service.CancelPreviousJobs(
ctx,
repo.ID,
from,
@@ -578,7 +579,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_model.CancelPreviousJobs(
+ if err := actions_service.CancelPreviousJobs(
ctx,
repo.ID,
oldDefaultBranchName,
diff --git a/services/repository/cache.go b/services/repository/cache.go
index b0811a99fc..cd5b95afa3 100644
--- a/services/repository/cache.go
+++ b/services/repository/cache.go
@@ -6,9 +6,9 @@ package repository
import (
"context"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
)
// CacheRef cachhe last commit information of the branch or the tag
diff --git a/services/repository/check.go b/services/repository/check.go
index 5cdcc14679..7e680f3c58 100644
--- a/services/repository/check.go
+++ b/services/repository/check.go
@@ -9,14 +9,14 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ system_model "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/util"
"xorm.io/builder"
)
diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go
index dccc124748..7a0d7edb7f 100644
--- a/services/repository/collaboration.go
+++ b/services/repository/collaboration.go
@@ -7,10 +7,10 @@ package repository
import (
"context"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
)
// DeleteCollaboration removes collaboration relation between the user and repository.
diff --git a/services/repository/collaboration_test.go b/services/repository/collaboration_test.go
index c087018be4..b27b91be23 100644
--- a/services/repository/collaboration_test.go
+++ b/services/repository/collaboration_test.go
@@ -6,9 +6,9 @@ package repository
import (
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
"github.com/stretchr/testify/require"
)
diff --git a/services/repository/commit.go b/services/repository/commit.go
index e8c0262ef4..0ff4ea701e 100644
--- a/services/repository/commit.go
+++ b/services/repository/commit.go
@@ -7,8 +7,8 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/modules/util"
- gitea_ctx "code.gitea.io/gitea/services/context"
+ "forgejo.org/modules/util"
+ gitea_ctx "forgejo.org/services/context"
)
type ContainedLinks struct { // TODO: better name?
diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go
index 635b0b108e..03a62d0410 100644
--- a/services/repository/commitstatus/commitstatus.go
+++ b/services/repository/commitstatus/commitstatus.go
@@ -9,17 +9,17 @@ import (
"fmt"
"slices"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
- shared_automerge "code.gitea.io/gitea/services/shared/automerge"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
+ shared_automerge "forgejo.org/services/shared/automerge"
)
func getCacheKey(repoID int64, brancheName string) string {
diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go
index 48871813bd..1805bd5960 100644
--- a/services/repository/contributors_graph.go
+++ b/services/repository/contributors_graph.go
@@ -14,16 +14,16 @@ import (
"sync"
"time"
- "code.gitea.io/gitea/models/avatars"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/avatars"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
"code.forgejo.org/go-chi/cache"
)
@@ -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, fmt.Errorf("unexpected type in cache detected")
+ return nil, errors.New("unexpected type in cache detected")
}
}
diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go
index c62bef25a1..45af85272d 100644
--- a/services/repository/contributors_graph_test.go
+++ b/services/repository/contributors_graph_test.go
@@ -8,12 +8,12 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/test"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/test"
"code.forgejo.org/go-chi/cache"
"github.com/stretchr/testify/assert"
@@ -53,14 +53,14 @@ func TestRepository_ContributorsGraph(t *testing.T) {
keys = append(keys, k)
}
slices.Sort(keys)
- assert.EqualValues(t, []string{
+ assert.Equal(t, []string{
"ethantkoenig@gmail.com",
"jimmy.praet@telenet.be",
"jon@allspice.io",
"total", // generated summary
}, keys)
- assert.EqualValues(t, &ContributorData{
+ assert.Equal(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.EqualValues(t, &ContributorData{
+ assert.Equal(t, &ContributorData{
Name: "Total",
AvatarLink: "",
TotalCommits: 3,
diff --git a/services/repository/create.go b/services/repository/create.go
index 8a1118cc2b..4491b12497 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -12,18 +12,18 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/options"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/templates/vars"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/options"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/templates/vars"
+ "forgejo.org/modules/util"
)
// CreateRepoOptions contains the create repository options
diff --git a/services/repository/create_test.go b/services/repository/create_test.go
index 9cde285181..0a6c34b6fe 100644
--- a/services/repository/create_test.go
+++ b/services/repository/create_test.go
@@ -7,13 +7,13 @@ import (
"fmt"
"testing"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -147,3 +147,13 @@ 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 09213e5c65..f4124fb9e2 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -1,4 +1,5 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repository
@@ -7,29 +8,29 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
- actions_model "code.gitea.io/gitea/models/actions"
- activities_model "code.gitea.io/gitea/models/activities"
- admin_model "code.gitea.io/gitea/models/admin"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- packages_model "code.gitea.io/gitea/models/packages"
- access_model "code.gitea.io/gitea/models/perm/access"
- project_model "code.gitea.io/gitea/models/project"
- repo_model "code.gitea.io/gitea/models/repo"
- secret_model "code.gitea.io/gitea/models/secret"
- system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/models/webhook"
- actions_module "code.gitea.io/gitea/modules/actions"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- federation_service "code.gitea.io/gitea/services/federation"
+ "forgejo.org/models"
+ actions_model "forgejo.org/models/actions"
+ activities_model "forgejo.org/models/activities"
+ admin_model "forgejo.org/models/admin"
+ 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"
+ packages_model "forgejo.org/models/packages"
+ access_model "forgejo.org/models/perm/access"
+ project_model "forgejo.org/models/project"
+ repo_model "forgejo.org/models/repo"
+ secret_model "forgejo.org/models/secret"
+ system_model "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/models/webhook"
+ actions_module "forgejo.org/modules/actions"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ federation_service "forgejo.org/services/federation"
"xorm.io/builder"
)
@@ -89,6 +90,11 @@ 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 451a182155..0e88a29230 100644
--- a/services/repository/files/cherry_pick.go
+++ b/services/repository/files/cherry_pick.go
@@ -5,16 +5,17 @@ package files
import (
"context"
+ "errors"
"fmt"
"strings"
- "code.gitea.io/gitea/models"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/pull"
+ "forgejo.org/models"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/structs"
+ "forgejo.org/services/pull"
)
// CherryPick cherrypicks or reverts a commit to the given repository
@@ -79,21 +80,33 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod
right, base = base, right
}
- 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)
- }
+ 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)
+ }
- if conflict {
- return nil, fmt.Errorf("failed to merge due to conflicts")
- }
+ 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)
+ }
- treeHash, err := t.WriteTree()
- if err != nil {
- // likely non-sensical tree due to merge conflicts...
- return nil, err
+ 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
+ }
}
// Now commit the tree
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index e0dad29273..43c9048aaf 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -1,4 +1,5 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package files
@@ -6,15 +7,15 @@ package files
import (
"context"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/structs"
+ asymkey_model "forgejo.org/models/asymkey"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/structs"
)
// 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)
+ divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch, nil)
if err != nil {
return nil, err
}
@@ -38,7 +39,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 = "gpg.error.not_signed_commit"
+ verification.Reason = asymkey_model.NotSigned
}
return verification
}
diff --git a/services/repository/files/content.go b/services/repository/files/content.go
index 32517e8d91..d701508ff0 100644
--- a/services/repository/files/content.go
+++ b/services/repository/files/content.go
@@ -5,18 +5,19 @@ package files
import (
"context"
+ "errors"
"fmt"
"net/url"
"path"
"strings"
- "code.gitea.io/gitea/models"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
)
// ContentType repo content type
@@ -107,7 +108,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
@@ -178,12 +179,13 @@ 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(),
- Size: entry.Size(),
- URL: &selfURLString,
+ Name: entry.Name(),
+ Path: treePath,
+ SHA: entry.ID.String(),
+ LastCommitSHA: lastCommit.ID.String(),
+ LastCommitWhen: lastCommit.Committer.When,
+ Size: entry.Size(),
+ URL: &selfURLString,
Links: &api.FileLinksResponse{
Self: &selfURLString,
},
@@ -204,19 +206,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.Blob().GetBlobContent(1024)
+ targetFromContent, err := entry.LinkTarget()
if err != nil {
return nil, err
}
contentsResponse.Target = &targetFromContent
- } else if entry.IsSubModule() {
+ } else if entry.IsSubmodule() {
contentsResponse.Type = string(ContentTypeSubmodule)
- submoduleURL, err := commit.GetSubModule(treePath)
+ submodule, err := commit.GetSubmodule(treePath, entry)
if err != nil {
return nil, err
}
- if submoduleURL != "" {
- contentsResponse.SubmoduleGitURL = &submoduleURL
+ if submodule.URL != "" {
+ contentsResponse.SubmoduleGitURL = &submodule.URL
}
}
// Handle links
@@ -228,7 +230,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
@@ -249,20 +251,35 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref
return contentsResponse, nil
}
-// 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) {
+// 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) {
gitBlob, err := gitRepo.GetBlob(sha)
if err != nil {
return nil, err
}
- content := ""
- if gitBlob.Size() <= setting.API.DefaultMaxBlobSize {
- content, err = gitBlob.GetBlobContentBase64()
- if err != nil {
- return nil, err
- }
+ content, err := gitBlob.GetContentBase64(setting.API.DefaultMaxBlobSize)
+ if err != nil && !errors.As(err, &git.BlobTooLargeError{}) {
+ return nil, err
}
- return &api.GitBlobResponse{
+
+ return &api.GitBlob{
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 f5e2b84690..8fc8f56b4f 100644
--- a/services/repository/files/content_test.go
+++ b/services/repository/files/content_test.go
@@ -5,15 +5,16 @@ package files
import (
"testing"
+ "time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/gitrepo"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/gitrepo"
+ api "forgejo.org/modules/structs"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -33,18 +34,19 @@ 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",
- Type: "file",
- Size: 30,
- Encoding: &encoding,
- Content: &content,
- URL: &selfURL,
- HTMLURL: &htmlURL,
- GitURL: &gitURL,
- DownloadURL: &downloadURL,
+ 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,
Links: &api.FileLinksResponse{
Self: &selfURL,
GitURL: &gitURL,
@@ -64,13 +66,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.EqualValues(t, expectedContentsResponse, fileContentResponse)
+ assert.Equal(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.EqualValues(t, expectedContentsResponse, fileContentResponse)
+ assert.Equal(t, expectedContentsResponse, fileContentResponse)
require.NoError(t, err)
})
}
@@ -190,7 +192,7 @@ func TestGetBlobBySHA(t *testing.T) {
defer gitRepo.Close()
gbr, err := GetBlobBySHA(db.DefaultContext, repo, gitRepo, "65f1bf27bc3bf70f64657658635e66094edbcb4d")
- expectedGBR := &api.GitBlobResponse{
+ expectedGBR := &api.GitBlob{
Content: "dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK",
Encoding: "base64",
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d",
@@ -200,3 +202,43 @@ 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.go b/services/repository/files/diff.go
index bf8b938e21..354a343d12 100644
--- a/services/repository/files/diff.go
+++ b/services/repository/files/diff.go
@@ -7,8 +7,8 @@ import (
"context"
"strings"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/services/gitdiff"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/services/gitdiff"
)
// GetDiffPreview produces and returns diff result of a file which is not yet committed.
diff --git a/services/repository/files/diff_test.go b/services/repository/files/diff_test.go
index 95de10e07e..67c63803d3 100644
--- a/services/repository/files/diff_test.go
+++ b/services/repository/files/diff_test.go
@@ -6,12 +6,12 @@ package files
import (
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/services/contexttest"
- "code.gitea.io/gitea/services/gitdiff"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/json"
+ "forgejo.org/services/contexttest"
+ "forgejo.org/services/gitdiff"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -124,7 +124,7 @@ func TestGetDiffPreview(t *testing.T) {
require.NoError(t, err)
bs, err := json.Marshal(diff)
require.NoError(t, err)
- assert.EqualValues(t, string(expectedBs), string(bs))
+ assert.Equal(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.EqualValues(t, expectedBs, bs)
+ assert.Equal(t, expectedBs, bs)
})
}
diff --git a/services/repository/files/file.go b/services/repository/files/file.go
index 7884d880e0..5b93258840 100644
--- a/services/repository/files/file.go
+++ b/services/repository/files/file.go
@@ -5,16 +5,16 @@ package files
import (
"context"
- "fmt"
+ "errors"
"net/url"
"strings"
"time"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
)
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, branch string, treeNames []string) (*api.FilesResponse, error) {
@@ -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, fmt.Errorf("repo cannot be nil")
+ return nil, errors.New("repo cannot be nil")
}
if commit == nil {
- return nil, fmt.Errorf("commit cannot be nil")
+ return nil, errors.New("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,36 +104,35 @@ 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.
- 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
+ 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
}
// Use the provided email and not revert to placeholder mail.
- 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,
- }
+ 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 = 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 db2f4403f4..169cafba0d 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.EqualValues(t, expectedCleanName, cleanName)
+ assert.Equal(t, expectedCleanName, cleanName)
})
t.Run("Clean a .git path", func(t *testing.T) {
name := "this/is/test/.git"
cleanName := CleanUploadFileName(name)
expectedCleanName := ""
- assert.EqualValues(t, expectedCleanName, cleanName)
+ assert.Equal(t, expectedCleanName, cleanName)
})
}
diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go
index e5f7e2af96..18b5226c02 100644
--- a/services/repository/files/patch.go
+++ b/services/repository/files/patch.go
@@ -8,15 +8,15 @@ import (
"fmt"
"strings"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/structs"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/structs"
+ asymkey_service "forgejo.org/services/asymkey"
)
// ApplyDiffPatchOptions holds the repository diff patch update options
@@ -147,11 +147,7 @@ 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")
- if git.CheckGitVersionAtLeast("2.32") == nil {
- cmdApply.AddArguments("-3")
- }
-
+ cmdApply := git.NewCommand(ctx, "apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary", "-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 30d95ba9ab..64d3e5887d 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -6,6 +6,7 @@ package files
import (
"bytes"
"context"
+ "errors"
"fmt"
"io"
"os"
@@ -13,15 +14,15 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- "code.gitea.io/gitea/services/gitdiff"
+ "forgejo.org/models"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ asymkey_service "forgejo.org/services/asymkey"
+ "forgejo.org/services/gitdiff"
)
// TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone
@@ -368,7 +369,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, fmt.Errorf("repository has not been cloned")
+ return nil, errors.New("repository has not been cloned")
}
return t.gitRepo.GetBranchCommit(branch)
}
@@ -376,7 +377,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, fmt.Errorf("repository has not been cloned")
+ return nil, errors.New("repository has not been cloned")
}
return t.gitRepo.GetCommit(commitID)
}
diff --git a/services/repository/files/temp_repo_test.go b/services/repository/files/temp_repo_test.go
index e7d85ea3cc..852e762267 100644
--- a/services/repository/files/temp_repo_test.go
+++ b/services/repository/files/temp_repo_test.go
@@ -6,10 +6,10 @@ package files
import (
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/git"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/git"
"github.com/stretchr/testify/require"
)
diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go
index e3a7f3b8b0..5a369b27a5 100644
--- a/services/repository/files/tree.go
+++ b/services/repository/files/tree.go
@@ -8,11 +8,11 @@ import (
"fmt"
"net/url"
- "code.gitea.io/gitea/models"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
)
// GetTreeBySHA get the GitTreeResponse of a repository using a sha hash.
@@ -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 9e5c5c1701..5cd628722b 100644
--- a/services/repository/files/tree_test.go
+++ b/services/repository/files/tree_test.go
@@ -6,9 +6,9 @@ package files
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/contexttest"
+ "forgejo.org/models/unittest"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/services/contexttest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -48,5 +48,5 @@ func TestGetTreeBySHA(t *testing.T) {
TotalCount: 1,
}
- assert.EqualValues(t, expectedTree, tree)
+ assert.Equal(t, expectedTree, tree)
}
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index d6025b6ced..8fb9644fa4 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -11,17 +11,17 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
+ "forgejo.org/models"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ asymkey_service "forgejo.org/services/asymkey"
)
// IdentityOptions for a person's identity like an author or committer
@@ -193,28 +193,34 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
if hasOldBranch {
- // Get the commit of the original branch
- commit, err := t.GetBranchCommit(opts.OldBranch)
+ // Get the current commit of the original branch
+ actualBaseCommit, err := t.GetBranchCommit(opts.OldBranch)
if err != nil {
return nil, err // Couldn't get a commit for the branch
}
- // 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)
+ 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)
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, commit, opts); err != nil {
+ if err := handleCheckErrors(file, actualBaseCommit, lastKnownCommit); err != nil {
return nil, err
}
}
+
+ if opts.LastCommitID == "" {
+ // needed for t.CommitTree
+ opts.LastCommitID = actualBaseCommit.ID.String()
+ }
}
contentStore := lfs.NewContentStore()
@@ -277,9 +283,9 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
}
// handles the check for various issues for ChangeRepoFiles
-func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error {
+func handleCheckErrors(file *ChangeRepoFile, actualBaseCommit *git.Commit, lastKnownCommit git.ObjectID) error {
if file.Operation == "update" || file.Operation == "delete" {
- fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath)
+ fromEntry, err := actualBaseCommit.GetTreeEntryByPath(file.Options.fromTreePath)
if err != nil {
return err
}
@@ -292,22 +298,22 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
CurrentSHA: fromEntry.ID.String(),
}
}
- } 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 {
+ } 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 {
return err
} else if changed {
return models.ErrCommitIDDoesNotMatch{
- GivenCommitID: opts.LastCommitID,
- CurrentCommitID: opts.LastCommitID,
+ GivenCommitID: lastKnownCommit.String(),
+ CurrentCommitID: actualBaseCommit.ID.String(),
}
}
- // The file wasn't modified, so we are good to delete it
+ // The file wasn't modified, so we are good to update it
}
} else {
- // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits
+ // When updating a file, a lastKnownCommit 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{}
}
@@ -322,7 +328,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep
subTreePath := ""
for index, part := range treePathParts {
subTreePath = path.Join(subTreePath, part)
- entry, err := commit.GetTreeEntryByPath(subTreePath)
+ entry, err := actualBaseCommit.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/files/upload.go b/services/repository/files/upload.go
index 1330116889..6359087e88 100644
--- a/services/repository/files/upload.go
+++ b/services/repository/files/upload.go
@@ -10,12 +10,12 @@ import (
"path"
"strings"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/setting"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/setting"
)
// UploadRepoFileOptions contains the uploaded repository file options
diff --git a/services/repository/fork.go b/services/repository/fork.go
index 0378f7bae6..9d15b6207d 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -9,17 +9,17 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ notify_service "forgejo.org/services/notify"
)
// ErrForkAlreadyExist represents a "ForkAlreadyExist" kind of error.
diff --git a/services/repository/fork_test.go b/services/repository/fork_test.go
index 2e1e72aaad..6de241e4d4 100644
--- a/services/repository/fork_test.go
+++ b/services/repository/fork_test.go
@@ -6,11 +6,12 @@ package repository
import (
"testing"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ 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"
@@ -36,7 +37,7 @@ func TestForkRepository(t *testing.T) {
assert.False(t, repo_model.IsErrReachLimitOfRepo(err))
// change AllowForkWithoutMaximumLimit to false for the test
- setting.Repository.AllowForkWithoutMaximumLimit = false
+ defer test.MockVariableValue(&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 4a312a33c3..e23e294de1 100644
--- a/services/repository/generate.go
+++ b/services/repository/generate.go
@@ -16,14 +16,14 @@ import (
"strings"
"time"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/util"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/util"
"github.com/gobwas/glob"
"github.com/huandu/xstrings"
@@ -43,12 +43,8 @@ type expansion struct {
var defaultTransformers = []transformer{
{Name: "SNAKE", Transform: xstrings.ToSnakeCase},
{Name: "KEBAB", Transform: xstrings.ToKebabCase},
- // 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: "CAMEL", Transform: xstrings.ToCamelCase},
+ {Name: "PASCAL", Transform: xstrings.ToPascalCase},
{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 b0f97d0ffb..2eb3a55e96 100644
--- a/services/repository/generate_test.go
+++ b/services/repository/generate_test.go
@@ -65,3 +65,30 @@ 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/gitgraph/graph.go b/services/repository/gitgraph/graph.go
index 4db5598015..bf15baed2a 100644
--- a/services/repository/gitgraph/graph.go
+++ b/services/repository/gitgraph/graph.go
@@ -10,8 +10,8 @@ import (
"os"
"strings"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/setting"
)
// GetCommitGraph return a list of commit (GraphItems) from all branches
diff --git a/services/repository/gitgraph/graph_models.go b/services/repository/gitgraph/graph_models.go
index 4e94468205..20107cc646 100644
--- a/services/repository/gitgraph/graph_models.go
+++ b/services/repository/gitgraph/graph_models.go
@@ -10,13 +10,13 @@ import (
"strings"
"time"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
+ asymkey_model "forgejo.org/models/asymkey"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
)
// NewGraph creates a basic graph
diff --git a/services/repository/gitgraph/graph_test.go b/services/repository/gitgraph/graph_test.go
index e7e437e42d..374341b276 100644
--- a/services/repository/gitgraph/graph_test.go
+++ b/services/repository/gitgraph/graph_test.go
@@ -9,7 +9,7 @@ import (
"strings"
"testing"
- "code.gitea.io/gitea/modules/git"
+ "forgejo.org/modules/git"
)
func BenchmarkGetCommitGraph(b *testing.B) {
diff --git a/services/repository/hooks.go b/services/repository/hooks.go
index 97e9e290a3..d3021414cf 100644
--- a/services/repository/hooks.go
+++ b/services/repository/hooks.go
@@ -7,12 +7,12 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/webhook"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
"xorm.io/builder"
)
diff --git a/services/repository/init.go b/services/repository/init.go
index 817fa4abd7..525b322752 100644
--- a/services/repository/init.go
+++ b/services/repository/init.go
@@ -9,13 +9,13 @@ import (
"os"
"time"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ asymkey_service "forgejo.org/services/asymkey"
)
// initRepoCommit temporarily changes with work directory.
diff --git a/services/repository/lfs.go b/services/repository/lfs.go
index 4cd1110e55..2e090290a7 100644
--- a/services/repository/lfs.go
+++ b/services/repository/lfs.go
@@ -8,14 +8,14 @@ import (
"fmt"
"time"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
)
// GarbageCollectLFSMetaObjectsOptions provides options for GarbageCollectLFSMetaObjects function
@@ -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.Pointer.StringContent()))
+ pointerSha := git.ComputeBlobHash(objectFormat, []byte(metaObject.StringContent()))
if gitRepo.IsObjectExist(pointerSha.String()) {
return git_model.MarkLFSMetaObject(ctx, metaObject.ID)
diff --git a/services/repository/lfs_test.go b/services/repository/lfs_test.go
index 838386d845..e38c38e29c 100644
--- a/services/repository/lfs_test.go
+++ b/services/repository/lfs_test.go
@@ -8,14 +8,14 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- repo_service "code.gitea.io/gitea/services/repository"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/storage"
+ repo_service "forgejo.org/services/repository"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/repository/main_test.go b/services/repository/main_test.go
index 7ad1540aee..942a805638 100644
--- a/services/repository/main_test.go
+++ b/services/repository/main_test.go
@@ -6,7 +6,7 @@ package repository
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/unittest"
)
func TestMain(m *testing.M) {
diff --git a/services/repository/migrate.go b/services/repository/migrate.go
index c8f65dd63d..80f5d68231 100644
--- a/services/repository/migrate.go
+++ b/services/repository/migrate.go
@@ -10,18 +10,18 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/lfs"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/migration"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/lfs"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/migration"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
)
// MigrateRepositoryGitData starts migrating git related data after created migrating repository
diff --git a/services/repository/push.go b/services/repository/push.go
index 924f365c05..eaedd80e1f 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -10,23 +10,23 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/cache"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/queue"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- issue_service "code.gitea.io/gitea/services/issue"
- notify_service "code.gitea.io/gitea/services/notify"
- pull_service "code.gitea.io/gitea/services/pull"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/cache"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/queue"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ issue_service "forgejo.org/services/issue"
+ notify_service "forgejo.org/services/notify"
+ pull_service "forgejo.org/services/pull"
)
// pushQueue represents a queue to handle update pull request tests
@@ -66,7 +66,7 @@ func PushUpdates(opts []*repo_module.PushUpdateOptions) error {
for _, opt := range opts {
if opt.IsNewRef() && opt.IsDelRef() {
- return fmt.Errorf("Old and new revisions are both NULL")
+ return errors.New("Old and new revisions are both NULL")
}
}
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 35bcdfd528..0d5ce647e0 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -6,23 +6,24 @@ package repository
import (
"context"
+ "errors"
"fmt"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- system_model "code.gitea.io/gitea/models/system"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- notify_service "code.gitea.io/gitea/services/notify"
- pull_service "code.gitea.io/gitea/services/pull"
+ "forgejo.org/models/db"
+ "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ repo_model "forgejo.org/models/repo"
+ system_model "forgejo.org/models/system"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ notify_service "forgejo.org/services/notify"
+ pull_service "forgejo.org/services/pull"
)
// WebSearchRepository represents a repository returned by web search
@@ -72,10 +73,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, fmt.Errorf("cannot push-create repository for org")
+ return nil, errors.New("cannot push-create repository for org")
}
} else if authUser.ID != owner.ID {
- return nil, fmt.Errorf("cannot push-create repository for another user")
+ return nil, errors.New("cannot push-create repository for another user")
}
}
@@ -118,6 +119,17 @@ 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 a5c0b3efcd..9f71bdec23 100644
--- a/services/repository/repository_test.go
+++ b/services/repository/repository_test.go
@@ -6,10 +6,10 @@ package repository
import (
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -41,3 +41,16 @@ 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/review.go b/services/repository/review.go
index 40513e6bc6..c4000a2846 100644
--- a/services/repository/review.go
+++ b/services/repository/review.go
@@ -6,9 +6,9 @@ package repository
import (
"context"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- repo_model "code.gitea.io/gitea/models/repo"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ repo_model "forgejo.org/models/repo"
)
// GetReviewerTeams get all teams can be requested to review
diff --git a/services/repository/review_test.go b/services/repository/review_test.go
index eb1712c2ce..5ece99a2e3 100644
--- a/services/repository/review_test.go
+++ b/services/repository/review_test.go
@@ -6,9 +6,9 @@ package repository
import (
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/repository/setting.go b/services/repository/setting.go
index 33b00cca8c..68cdfc370b 100644
--- a/services/repository/setting.go
+++ b/services/repository/setting.go
@@ -7,12 +7,11 @@ import (
"context"
"slices"
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/log"
- actions_service "code.gitea.io/gitea/services/actions"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unit"
+ "forgejo.org/modules/log"
+ actions_service "forgejo.org/services/actions"
)
// UpdateRepositoryUnits updates a repository's units
@@ -29,7 +28,7 @@ func UpdateRepositoryUnits(ctx context.Context, repo *repo_model.Repository, uni
}
if slices.Contains(deleteUnitTypes, unit.TypeActions) {
- if err := actions_model.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
+ if err := actions_service.CleanRepoScheduleTasks(ctx, repo, true); err != nil {
log.Error("CleanRepoScheduleTasks: %v", err)
}
}
diff --git a/services/repository/star.go b/services/repository/star.go
index 505da0f099..8cc2e0a243 100644
--- a/services/repository/star.go
+++ b/services/repository/star.go
@@ -6,10 +6,10 @@ package repository
import (
"context"
- "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/services/federation"
+ "forgejo.org/models/repo"
+ "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/services/federation"
)
func StarRepoAndSendLikeActivities(ctx context.Context, doer user.User, repoID int64, star bool) error {
diff --git a/services/repository/sync_fork.go b/services/repository/sync_fork.go
new file mode 100644
index 0000000000..ebcac76136
--- /dev/null
+++ b/services/repository/sync_fork.go
@@ -0,0 +1,113 @@
+// 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/repository/template.go b/services/repository/template.go
index 36a680c8e2..3566aa2b7e 100644
--- a/services/repository/template.go
+++ b/services/repository/template.go
@@ -6,12 +6,12 @@ package repository
import (
"context"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ notify_service "forgejo.org/services/notify"
)
// GenerateIssueLabels generates issue labels from a template repository
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index 467c85ef6f..6026d85ae1 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -9,19 +9,19 @@ import (
"os"
"strings"
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/sync"
- "code.gitea.io/gitea/modules/util"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/sync"
+ "forgejo.org/modules/util"
+ notify_service "forgejo.org/services/notify"
)
// repoWorkingPool represents a working pool to order the parallel changes to the same repository
diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go
index cc51a05781..4bb0fc140c 100644
--- a/services/repository/transfer_test.go
+++ b/services/repository/transfer_test.go
@@ -7,17 +7,17 @@ import (
"sync"
"testing"
- "code.gitea.io/gitea/models"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/feed"
- notify_service "code.gitea.io/gitea/services/notify"
+ "forgejo.org/models"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/db"
+ "forgejo.org/models/organization"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/feed"
+ notify_service "forgejo.org/services/notify"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/secrets/secrets.go b/services/secrets/secrets.go
index 031c474dd7..2de83ef5a2 100644
--- a/services/secrets/secrets.go
+++ b/services/secrets/secrets.go
@@ -6,8 +6,8 @@ package secrets
import (
"context"
- "code.gitea.io/gitea/models/db"
- secret_model "code.gitea.io/gitea/models/secret"
+ "forgejo.org/models/db"
+ secret_model "forgejo.org/models/secret"
)
func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*secret_model.Secret, bool, error) {
@@ -15,16 +15,16 @@ func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data
return nil, false, err
}
- s, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{
+ s, exists, err := db.Get[secret_model.Secret](ctx, secret_model.FindSecretsOptions{
OwnerID: ownerID,
RepoID: repoID,
Name: name,
- })
+ }.ToConds())
if err != nil {
return nil, false, err
}
- if len(s) == 0 {
+ if !exists {
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
}
- if err := secret_model.UpdateSecret(ctx, s[0].ID, data); err != nil {
+ s.SetSecret(data)
+ if _, err := db.GetEngine(ctx).Cols("data").ID(s.ID).Update(s); err != nil {
return nil, false, err
}
-
- return s[0], false, nil
+ return s, false, nil
}
func DeleteSecretByID(ctx context.Context, ownerID, repoID, secretID int64) error {
diff --git a/services/secrets/validation.go b/services/secrets/validation.go
index 3db5b96452..44250ba87b 100644
--- a/services/secrets/validation.go
+++ b/services/secrets/validation.go
@@ -6,7 +6,7 @@ package secrets
import (
"regexp"
- "code.gitea.io/gitea/modules/util"
+ "forgejo.org/modules/util"
)
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
diff --git a/services/shared/automerge/automerge.go b/services/shared/automerge/automerge.go
index 8f38cf260a..be7b2f6eb4 100644
--- a/services/shared/automerge/automerge.go
+++ b/services/shared/automerge/automerge.go
@@ -9,21 +9,21 @@ import (
"strconv"
"strings"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/queue"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/queue"
)
// PRAutoMergeQueue represents a queue to handle update pull request tests
var PRAutoMergeQueue *queue.WorkerPoolQueue[string]
func addToQueue(pr *issues_model.PullRequest, sha string) {
- log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
+ log.Trace("Adding pullID: %d to the automerge 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 pull requests patch checking queue %v", pr.ID, err)
+ log.Error("Error adding pullID: %d to the automerge queue %v", pr.ID, err)
}
}
@@ -43,32 +43,29 @@ 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
}
- if err := pull.LoadBaseRepo(ctx); err != nil {
- log.Error("LoadBaseRepo: %v", err)
- return
+ commitID := pull.HeadCommitID
+ if commitID == "" {
+ commitID = getCommitIDFromRefName(ctx, pull)
}
- 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)
+ if commitID == "" {
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 {
@@ -118,3 +115,24 @@ 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/migrate.go b/services/task/migrate.go
index 9cef77a6c8..a9f76299fd 100644
--- a/services/task/migrate.go
+++ b/services/task/migrate.go
@@ -10,20 +10,20 @@ import (
"strings"
"time"
- admin_model "code.gitea.io/gitea/models/admin"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/migrations"
- notify_service "code.gitea.io/gitea/services/notify"
+ admin_model "forgejo.org/models/admin"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/migration"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/migrations"
+ notify_service "forgejo.org/services/notify"
)
func handleCreateError(owner *user_model.User, err error) error {
diff --git a/services/task/task.go b/services/task/task.go
index ac659ac3e5..f030bdb38c 100644
--- a/services/task/task.go
+++ b/services/task/task.go
@@ -5,23 +5,24 @@ package task
import (
"context"
+ "errors"
"fmt"
- admin_model "code.gitea.io/gitea/models/admin"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/secret"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/timeutil"
- "code.gitea.io/gitea/modules/util"
- repo_service "code.gitea.io/gitea/services/repository"
+ admin_model "forgejo.org/models/admin"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ base "forgejo.org/modules/migration"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/secret"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/modules/timeutil"
+ "forgejo.org/modules/util"
+ repo_service "forgejo.org/services/repository"
)
// taskQueue is a global queue of tasks
@@ -41,7 +42,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 fmt.Errorf("unable to create task queue")
+ return errors.New("unable to create task queue")
}
go graceful.GetManager().RunWithCancel(taskQueue)
return nil
diff --git a/services/uinotification/notify.go b/services/uinotification/notify.go
index be5f7019a2..25048e7b53 100644
--- a/services/uinotification/notify.go
+++ b/services/uinotification/notify.go
@@ -6,16 +6,16 @@ package uinotification
import (
"context"
- activities_model "code.gitea.io/gitea/models/activities"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/queue"
- notify_service "code.gitea.io/gitea/services/notify"
+ activities_model "forgejo.org/models/activities"
+ "forgejo.org/models/db"
+ issues_model "forgejo.org/models/issues"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/container"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/queue"
+ notify_service "forgejo.org/services/notify"
)
type (
diff --git a/services/user/TestReplaceInactivePrimaryEmail/forgejo_auth_token.yml b/services/user/TestReplaceInactivePrimaryEmail/forgejo_auth_token.yml
new file mode 100644
index 0000000000..93a6c184b6
--- /dev/null
+++ b/services/user/TestReplaceInactivePrimaryEmail/forgejo_auth_token.yml
@@ -0,0 +1,5 @@
+-
+ id: 1001
+ uid: 10
+ lookup_key: unique
+ purpose: user_activation
diff --git a/services/user/avatar.go b/services/user/avatar.go
index 3f87466eaa..79dfc76503 100644
--- a/services/user/avatar.go
+++ b/services/user/avatar.go
@@ -10,11 +10,11 @@ import (
"io"
"os"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/avatar"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/storage"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/avatar"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/storage"
)
// UploadAvatar saves custom avatar for user.
diff --git a/services/user/avatar_test.go b/services/user/avatar_test.go
index 21fca8dd09..17132a74ab 100644
--- a/services/user/avatar_test.go
+++ b/services/user/avatar_test.go
@@ -10,11 +10,11 @@ import (
"os"
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/test"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/storage"
+ "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.NotEqual(t, "", verification.Avatar)
+ assert.NotEmpty(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.Equal(t, "", verification.Avatar)
+ assert.Empty(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.NotEqual(t, "", verification.Avatar)
+ assert.NotEmpty(t, verification.Avatar)
err = DeleteAvatar(db.DefaultContext, user)
require.NoError(t, err)
verification = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
- assert.Equal(t, "", verification.Avatar)
+ assert.Empty(t, verification.Avatar)
})
}
diff --git a/services/user/block.go b/services/user/block.go
index 0b31119dfb..6be8dc5f70 100644
--- a/services/user/block.go
+++ b/services/user/block.go
@@ -5,10 +5,10 @@ package user
import (
"context"
- model "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
+ model "forgejo.org/models"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
"xorm.io/builder"
)
diff --git a/services/user/block_test.go b/services/user/block_test.go
index 13959e56b4..a2ba5d71a7 100644
--- a/services/user/block_test.go
+++ b/services/user/block_test.go
@@ -6,12 +6,12 @@ package user
import (
"testing"
- model "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
+ model "forgejo.org/models"
+ "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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/user/delete.go b/services/user/delete.go
index 587e3c2a8f..bed7abde07 100644
--- a/services/user/delete.go
+++ b/services/user/delete.go
@@ -1,4 +1,5 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@@ -10,20 +11,20 @@ import (
_ "image/jpeg" // Needed for jpeg support
- actions_model "code.gitea.io/gitea/models/actions"
- activities_model "code.gitea.io/gitea/models/activities"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- pull_model "code.gitea.io/gitea/models/pull"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- issue_service "code.gitea.io/gitea/services/issue"
+ actions_model "forgejo.org/models/actions"
+ activities_model "forgejo.org/models/activities"
+ asymkey_model "forgejo.org/models/asymkey"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ git_model "forgejo.org/models/git"
+ issues_model "forgejo.org/models/issues"
+ "forgejo.org/models/organization"
+ access_model "forgejo.org/models/perm/access"
+ pull_model "forgejo.org/models/pull"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ issue_service "forgejo.org/services/issue"
"xorm.io/builder"
)
@@ -216,6 +217,11 @@ 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 31404aadaa..cc38dacf95 100644
--- a/services/user/email.go
+++ b/services/user/email.go
@@ -1,4 +1,5 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
+// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@@ -8,12 +9,13 @@ import (
"errors"
"strings"
- "code.gitea.io/gitea/models/db"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/validation"
- "code.gitea.io/gitea/services/mailer"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/util"
+ "forgejo.org/modules/validation"
+ "forgejo.org/services/mailer"
)
// AdminAddOrSetPrimaryEmailAddress is used by admins to add or set a user's primary email address
@@ -170,6 +172,11 @@ 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})
}
@@ -203,6 +210,11 @@ 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 86f31a8984..d2cff22696 100644
--- a/services/user/email_test.go
+++ b/services/user/email_test.go
@@ -6,11 +6,12 @@ package user
import (
"testing"
- "code.gitea.io/gitea/models/db"
- organization_model "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
+ auth_model "forgejo.org/models/auth"
+ "forgejo.org/models/db"
+ organization_model "forgejo.org/models/organization"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/setting"
"github.com/gobwas/glob"
"github.com/stretchr/testify/assert"
@@ -123,25 +124,34 @@ func TestAddEmailAddresses(t *testing.T) {
}
func TestReplaceInactivePrimaryEmail(t *testing.T) {
+ defer unittest.OverrideFixtures("services/user/TestReplaceInactivePrimaryEmail/")()
require.NoError(t, unittest.PrepareTestDatabase())
- 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("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: "user201@example.com",
- UID: 10,
- }
- err = ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
- require.NoError(t, err)
+ t.Run("Normal", func(t *testing.T) {
+ unittest.AssertExistsIf(t, true, &auth_model.AuthorizationToken{UID: 10})
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
- assert.Equal(t, "user201@example.com", user.Email)
+ 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})
+ })
}
func TestDeleteEmailAddresses(t *testing.T) {
diff --git a/services/user/update.go b/services/user/update.go
index 62c30ac05f..65d3992751 100644
--- a/services/user/update.go
+++ b/services/user/update.go
@@ -7,14 +7,14 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
- auth_model "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- password_module "code.gitea.io/gitea/modules/auth/password"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/services/mailer"
+ "forgejo.org/models"
+ auth_model "forgejo.org/models/auth"
+ user_model "forgejo.org/models/user"
+ password_module "forgejo.org/modules/auth/password"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/structs"
+ "forgejo.org/services/mailer"
)
type UpdateOptions struct {
diff --git a/services/user/update_test.go b/services/user/update_test.go
index 11379d4508..f1754b2db9 100644
--- a/services/user/update_test.go
+++ b/services/user/update_test.go
@@ -6,12 +6,12 @@ package user
import (
"testing"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- password_module "code.gitea.io/gitea/modules/auth/password"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/structs"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ password_module "forgejo.org/modules/auth/password"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/user/user.go b/services/user/user.go
index 62fe44ca27..9cb6858f0c 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -11,24 +11,26 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- packages_model "code.gitea.io/gitea/models/packages"
- repo_model "code.gitea.io/gitea/models/repo"
- system_model "code.gitea.io/gitea/models/system"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/eventsource"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/storage"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/agit"
- org_service "code.gitea.io/gitea/services/org"
- "code.gitea.io/gitea/services/packages"
- container_service "code.gitea.io/gitea/services/packages/container"
- repo_service "code.gitea.io/gitea/services/repository"
+ "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"
+ repo_model "forgejo.org/models/repo"
+ system_model "forgejo.org/models/system"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/eventsource"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ "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"
+ repo_service "forgejo.org/services/repository"
)
// RenameUser renames a user
@@ -47,10 +49,28 @@ func renameUser(ctx context.Context, u *user_model.User, newUserName string, doe
}
// Non-local users are not allowed to change their username.
- if !u.IsOrganization() && !u.IsLocal() {
- return user_model.ErrUserIsNotLocal{
- UID: u.ID,
- Name: u.Name,
+ // 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,
+ }
}
}
diff --git a/services/user/user_test.go b/services/user/user_test.go
index 058ff7b6ed..7240813411 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -11,17 +11,22 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
- "code.gitea.io/gitea/modules/timeutil"
+ "forgejo.org/models"
+ 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"
@@ -67,13 +72,7 @@ func TestDeleteUser(t *testing.T) {
}
func TestPurgeUser(t *testing.T) {
- defer unittest.OverrideFixtures(
- unittest.FixturesOptions{
- Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
- Base: setting.AppWorkPath,
- Dirs: []string{"services/user/TestPurgeUser/"},
- },
- )()
+ defer unittest.OverrideFixtures("services/user/TestPurgeUser")()
require.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
defer test.MockVariableValue(&setting.SSH.CreateAuthorizedKeysFile, true)()
@@ -143,17 +142,10 @@ 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))
})
@@ -193,9 +185,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 := user_model.LookupUserRedirect(db.DefaultContext, oldUsername)
+ redirectUID, err := redirect_service.LookupUserRedirect(db.DefaultContext, user, oldUsername)
require.NoError(t, err)
- assert.EqualValues(t, user.ID, redirectUID)
+ assert.Equal(t, user.ID, redirectUID)
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})
})
@@ -211,17 +203,41 @@ func TestRenameUser(t *testing.T) {
unittest.AssertExistsIf(t, true, &user_model.Redirect{LowerName: "user_rename"})
// The granularity of created_unix is a second.
- time.Sleep(time.Second)
+ test.SleepTillNextSecond()
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
- time.Sleep(time.Second)
+ test.SleepTillNextSecond()
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) {
@@ -283,3 +299,56 @@ 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 089ff8bae3..797b98f99a 100644
--- a/services/webhook/default.go
+++ b/services/webhook/default.go
@@ -11,14 +11,14 @@ import (
"net/url"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/svg"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/svg"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
var _ Handler = defaultHandler{}
@@ -36,8 +36,7 @@ func (dh defaultHandler) Type() webhook_module.HookType {
func (dh defaultHandler) Icon(size int) template.HTML {
if dh.forgejo {
- // forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work
- return shared.ImgIcon("forgejo.svg", size)
+ return svg.RenderHTML("gitea-forgejo", size, "img")
}
return svg.RenderHTML("gitea-gitea", size, "img")
}
diff --git a/services/webhook/default_test.go b/services/webhook/default_test.go
index 7056e77b47..fcef4612e1 100644
--- a/services/webhook/default_test.go
+++ b/services/webhook/default_test.go
@@ -6,9 +6,9 @@ package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ webhook_module "forgejo.org/modules/webhook"
jsoniter "github.com/json-iterator/go"
"github.com/stretchr/testify/assert"
@@ -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.Equal(t, "", j.Get("state").MustBeValid().ToString())
+ assert.Empty(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 25668143e6..23aca80345 100644
--- a/services/webhook/deliver.go
+++ b/services/webhook/deliver.go
@@ -6,6 +6,7 @@ package webhook
import (
"context"
"crypto/tls"
+ "errors"
"fmt"
"io"
"net/http"
@@ -14,16 +15,16 @@ import (
"sync"
"time"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/hostmatcher"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/proxy"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/timeutil"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/hostmatcher"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/process"
+ "forgejo.org/modules/proxy"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/timeutil"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/gobwas/glob"
)
@@ -218,7 +219,7 @@ func Init() error {
hookQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "webhook_sender", handler)
if hookQueue == nil {
- return fmt.Errorf("unable to create webhook_sender queue")
+ return errors.New("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 c6d1cb60dc..1a9ce05de4 100644
--- a/services/webhook/deliver_test.go
+++ b/services/webhook/deliver_test.go
@@ -12,13 +12,13 @@ import (
"testing"
"time"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/unittest"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/hostmatcher"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ "forgejo.org/models/db"
+ "forgejo.org/models/unittest"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/hostmatcher"
+ "forgejo.org/modules/setting"
+ "forgejo.org/modules/test"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.Equal(t, "", r.Header.Get("Content-Type"))
+ assert.Empty(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 899c5b2d9f..ec53c79c2c 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -11,13 +11,13 @@ import (
"net/url"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type dingtalkHandler struct{}
@@ -207,6 +207,12 @@ 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/dingtalk_test.go b/services/webhook/dingtalk_test.go
index 762d29dddc..5d2a240660 100644
--- a/services/webhook/dingtalk_test.go
+++ b/services/webhook/dingtalk_test.go
@@ -7,10 +7,10 @@ import (
"net/url"
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index cd25175ea1..7259c4a995 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -15,17 +15,18 @@ import (
"strings"
"unicode/utf8"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- gitea_context "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/base"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ webhook_module "forgejo.org/modules/webhook"
+ gitea_context "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
"code.forgejo.org/go-chi/binding"
)
@@ -151,6 +152,18 @@ 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
@@ -312,9 +325,10 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error)
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
}
-type discordConvertor struct {
- Username string
- AvatarURL string
+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
}
var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{}
@@ -336,7 +350,7 @@ func parseHookPullRequestEventType(event webhook_module.HookEventType) (string,
case webhook_module.HookEventPullRequestReviewApproved:
return "approved", nil
case webhook_module.HookEventPullRequestReviewRejected:
- return "rejected", nil
+ return "requested changes", nil
case webhook_module.HookEventPullRequestReviewComment:
return "comment", nil
default:
@@ -357,7 +371,7 @@ func (d discordConvertor) createPayload(s *api.User, title, text, url string, co
Embeds: []DiscordEmbed{
{
Title: title,
- Description: text,
+ Description: base.TruncateString(text, discordDescriptionCharactersLimit),
URL: url,
Color: color,
Author: DiscordEmbedAuthor{
diff --git a/services/webhook/discord_test.go b/services/webhook/discord_test.go
index e0bb2225f7..b04be30bc6 100644
--- a/services/webhook/discord_test.go
+++ b/services/webhook/discord_test.go
@@ -6,11 +6,11 @@ package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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, 4096)
+ assert.Len(t, pl.Embeds[0].Description, 2000)
})
t.Run("IssueComment", func(t *testing.T) {
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index f77c3bbd65..57f2362783 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -10,12 +10,12 @@ import (
"net/http"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type feishuHandler struct{}
@@ -191,6 +191,12 @@ 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/feishu_test.go b/services/webhook/feishu_test.go
index 614e0f1ef4..7cf24b84ed 100644
--- a/services/webhook/feishu_test.go
+++ b/services/webhook/feishu_test.go
@@ -6,10 +6,10 @@ package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/webhook/general.go b/services/webhook/general.go
index ef68f2885b..c728b6ba1a 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -9,11 +9,11 @@ import (
"net/url"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ webhook_module "forgejo.org/modules/webhook"
)
type linkFormatter = func(string, string) string
@@ -37,11 +37,12 @@ func getPullRequestInfo(p *api.PullRequestPayload) (title, link, by, operator, o
for i, user := range assignList {
assignStringList[i] = user.UserName
}
- if p.Action == api.HookIssueAssigned {
+ switch p.Action {
+ case api.HookIssueAssigned:
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
- } else if p.Action == api.HookIssueUnassigned {
+ case api.HookIssueUnassigned:
operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
- } else if p.Action == api.HookIssueMilestoned {
+ case api.HookIssueMilestoned:
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID)
}
link = p.PullRequest.HTMLURL
@@ -62,11 +63,12 @@ func getIssuesInfo(p *api.IssuePayload) (issueTitle, link, by, operator, operate
for i, user := range assignList {
assignStringList[i] = user.UserName
}
- if p.Action == api.HookIssueAssigned {
+ switch p.Action {
+ case api.HookIssueAssigned:
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
- } else if p.Action == api.HookIssueUnassigned {
+ case api.HookIssueUnassigned:
operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
- } else if p.Action == api.HookIssueMilestoned {
+ case api.HookIssueMilestoned:
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID)
}
link = p.Issue.HTMLURL
@@ -302,6 +304,25 @@ 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 8412293708..10c779742d 100644
--- a/services/webhook/general_test.go
+++ b/services/webhook/general_test.go
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
- api "code.gitea.io/gitea/modules/structs"
+ api "forgejo.org/modules/structs"
"github.com/stretchr/testify/assert"
)
@@ -270,6 +270,22 @@ 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,
@@ -675,3 +691,36 @@ 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/gogs.go b/services/webhook/gogs.go
index 7dbf64343f..bbab1ad41d 100644
--- a/services/webhook/gogs.go
+++ b/services/webhook/gogs.go
@@ -7,10 +7,10 @@ import (
"html/template"
"net/http"
- webhook_model "code.gitea.io/gitea/models/webhook"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type gogsHandler struct{ defaultHandler }
diff --git a/services/webhook/main_test.go b/services/webhook/main_test.go
index 6147aac499..97957291ca 100644
--- a/services/webhook/main_test.go
+++ b/services/webhook/main_test.go
@@ -6,13 +6,13 @@ package webhook
import (
"testing"
- "code.gitea.io/gitea/models/unittest"
- "code.gitea.io/gitea/modules/hostmatcher"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/models/unittest"
+ "forgejo.org/modules/hostmatcher"
+ "forgejo.org/modules/setting"
- _ "code.gitea.io/gitea/models"
- _ "code.gitea.io/gitea/models/actions"
- _ "code.gitea.io/gitea/models/forgefed"
+ _ "forgejo.org/models"
+ _ "forgejo.org/models/actions"
+ _ "forgejo.org/models/forgefed"
)
func TestMain(m *testing.M) {
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 4b33bfb1d3..bdb0c292ab 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -16,16 +16,16 @@ import (
"regexp"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/svg"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/svg"
+ "forgejo.org/modules/util"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type matrixHandler struct{}
@@ -273,6 +273,12 @@ 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/matrix_test.go b/services/webhook/matrix_test.go
index 46e0041a34..1644def0e1 100644
--- a/services/webhook/matrix_test.go
+++ b/services/webhook/matrix_test.go
@@ -6,10 +6,10 @@ package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 736d084a8c..3b35c407e1 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -11,13 +11,13 @@ import (
"net/url"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type msteamsHandler struct{}
@@ -326,6 +326,23 @@ 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 d9a9724e5b..da6439f198 100644
--- a/services/webhook/msteams_test.go
+++ b/services/webhook/msteams_test.go
@@ -6,10 +6,10 @@ package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.Equal(t, "", pl.Sections[0].Text)
+ assert.Empty(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.Equal(t, "", pl.Sections[0].Text)
+ assert.Empty(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 ddd7002890..009efc994f 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -6,20 +6,21 @@ package webhook
import (
"context"
- issues_model "code.gitea.io/gitea/models/issues"
- packages_model "code.gitea.io/gitea/models/packages"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/convert"
- notify_service "code.gitea.io/gitea/services/notify"
+ actions_model "forgejo.org/models/actions"
+ issues_model "forgejo.org/models/issues"
+ packages_model "forgejo.org/models/packages"
+ "forgejo.org/models/perm"
+ access_model "forgejo.org/models/perm/access"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/repository"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/convert"
+ notify_service "forgejo.org/services/notify"
)
func init() {
@@ -887,6 +888,45 @@ 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 36ec3b8bf1..a810de91c1 100644
--- a/services/webhook/notifier_test.go
+++ b/services/webhook/notifier_test.go
@@ -4,20 +4,22 @@
package webhook
import (
- "path/filepath"
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/test"
+ actions_model "forgejo.org/models/actions"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ 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"
@@ -56,13 +58,7 @@ func pushCommits() *repository.PushCommits {
}
func TestSyncPushCommits(t *testing.T) {
- defer unittest.OverrideFixtures(
- unittest.FixturesOptions{
- Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
- Base: setting.AppWorkPath,
- Dirs: []string{"services/webhook/TestPushCommits"},
- },
- )()
+ defer unittest.OverrideFixtures("services/webhook/TestPushCommits")()
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -90,18 +86,12 @@ 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.EqualValues(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
+ assert.Equal(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
})
}
func TestPushCommits(t *testing.T) {
- defer unittest.OverrideFixtures(
- unittest.FixturesOptions{
- Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
- Base: setting.AppWorkPath,
- Dirs: []string{"services/webhook/TestPushCommits"},
- },
- )()
+ defer unittest.OverrideFixtures("services/webhook/TestPushCommits")()
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -129,6 +119,193 @@ 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.EqualValues(t, "2c54faec6c45d31c1abfaecdab471eac6633738a", payloadContent.Commits[0].ID)
+ 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)
})
}
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index 9831a4e008..7ae3e0c48f 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -10,12 +10,12 @@ import (
"net/http"
"net/url"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type packagistHandler struct{}
diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go
index 0f696f1b99..e5bf4ec8d1 100644
--- a/services/webhook/packagist_test.go
+++ b/services/webhook/packagist_test.go
@@ -7,10 +7,10 @@ import (
"fmt"
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/webhook/shared/img.go b/services/webhook/shared/img.go
index 2d65ba4e0f..95286c563e 100644
--- a/services/webhook/shared/img.go
+++ b/services/webhook/shared/img.go
@@ -5,7 +5,7 @@ import (
"html/template"
"strconv"
- "code.gitea.io/gitea/modules/setting"
+ "forgejo.org/modules/setting"
)
func ImgIcon(name string, size int) template.HTML {
diff --git a/services/webhook/shared/payloader.go b/services/webhook/shared/payloader.go
index cf0bfa82cb..e3be4c4b4c 100644
--- a/services/webhook/shared/payloader.go
+++ b/services/webhook/shared/payloader.go
@@ -14,10 +14,10 @@ import (
"io"
"net/http"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
)
var ErrPayloadTypeNotSupported = errors.New("unsupported webhook event")
@@ -36,6 +36,7 @@ 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) {
@@ -86,6 +87,8 @@ 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 5ef3e4e06f..8c61e7ba25 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -11,15 +11,15 @@ import (
"regexp"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- gitea_context "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
+ gitea_context "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
"code.forgejo.org/go-chi/binding"
)
@@ -142,6 +142,7 @@ 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)
@@ -311,6 +312,12 @@ 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/slack_test.go b/services/webhook/slack_test.go
index ecc11d541f..62090fd310 100644
--- a/services/webhook/slack_test.go
+++ b/services/webhook/slack_test.go
@@ -6,10 +6,10 @@ package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/webhook/sourcehut/builds.go b/services/webhook/sourcehut/builds.go
index 346ccd3c0b..96ed55c8ff 100644
--- a/services/webhook/sourcehut/builds.go
+++ b/services/webhook/sourcehut/builds.go
@@ -13,20 +13,20 @@ import (
"net/http"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- gitea_context "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
+ gitea_context "forgejo.org/services/context"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
"code.forgejo.org/go-chi/binding"
- "gopkg.in/yaml.v3"
+ "go.yaml.in/yaml/v3"
)
type BuildsHandler struct{}
@@ -190,6 +190,10 @@ 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/sourcehut/builds_test.go b/services/webhook/sourcehut/builds_test.go
index 689c369a7f..ac4172f5ff 100644
--- a/services/webhook/sourcehut/builds_test.go
+++ b/services/webhook/sourcehut/builds_test.go
@@ -7,14 +7,14 @@ import (
"strings"
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/test"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/test"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/webhook/shared"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index a02a7691e9..e6897f68bc 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -11,15 +11,15 @@ import (
"net/url"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/json"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/markup"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type telegramHandler struct{}
@@ -205,6 +205,12 @@ 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/telegram_test.go b/services/webhook/telegram_test.go
index 85a62f7615..5066e55b8c 100644
--- a/services/webhook/telegram_test.go
+++ b/services/webhook/telegram_test.go
@@ -6,10 +6,10 @@ package webhook
import (
"testing"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/json"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/json"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go
index 1366ea8e8f..ecbbfcfbd6 100644
--- a/services/webhook/webhook.go
+++ b/services/webhook/webhook.go
@@ -11,21 +11,21 @@ import (
"net/http"
"strings"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/graceful"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/queue"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/sourcehut"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ user_model "forgejo.org/models/user"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/graceful"
+ "forgejo.org/modules/log"
+ "forgejo.org/modules/optional"
+ "forgejo.org/modules/queue"
+ "forgejo.org/modules/setting"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/sourcehut"
"github.com/gobwas/glob"
)
@@ -103,7 +103,7 @@ type EventSource struct {
Owner *user_model.User
}
-// handle delivers hook tasks
+// handler 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 2ebbbe4a51..15cb8f620c 100644
--- a/services/webhook/webhook_test.go
+++ b/services/webhook/webhook_test.go
@@ -7,15 +7,16 @@ import (
"fmt"
"testing"
- "code.gitea.io/gitea/models/db"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/setting"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/convert"
+ "forgejo.org/models/db"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ 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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -104,7 +105,8 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
func TestWebhookUserMail(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
- setting.Service.NoReplyAddress = "no-reply.com"
+ defer test.MockVariableValue(&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 87f8bb8b18..5c765b0754 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -10,12 +10,12 @@ import (
"net/http"
"strings"
- webhook_model "code.gitea.io/gitea/models/webhook"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- webhook_module "code.gitea.io/gitea/modules/webhook"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/webhook/shared"
+ webhook_model "forgejo.org/models/webhook"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
+ webhook_module "forgejo.org/modules/webhook"
+ "forgejo.org/services/forms"
+ "forgejo.org/services/webhook/shared"
)
type wechatworkHandler struct{}
@@ -201,6 +201,12 @@ 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.go b/services/wiki/wiki.go
index 63196aa862..cf1477e72c 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -11,17 +11,17 @@ import (
"os"
"strings"
- repo_model "code.gitea.io/gitea/models/repo"
- system_model "code.gitea.io/gitea/models/system"
- "code.gitea.io/gitea/models/unit"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/modules/log"
- repo_module "code.gitea.io/gitea/modules/repository"
- "code.gitea.io/gitea/modules/sync"
- asymkey_service "code.gitea.io/gitea/services/asymkey"
- repo_service "code.gitea.io/gitea/services/repository"
+ repo_model "forgejo.org/models/repo"
+ system_model "forgejo.org/models/system"
+ "forgejo.org/models/unit"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
+ "forgejo.org/modules/log"
+ repo_module "forgejo.org/modules/repository"
+ "forgejo.org/modules/sync"
+ asymkey_service "forgejo.org/services/asymkey"
+ repo_service "forgejo.org/services/repository"
)
// TODO: use clustered lock (unique queue? or *abuse* cache)
diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go
index 74c7064043..ca312388af 100644
--- a/services/wiki/wiki_path.go
+++ b/services/wiki/wiki_path.go
@@ -8,11 +8,11 @@ import (
"path"
"strings"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/modules/git"
- api "code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/convert"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/modules/git"
+ api "forgejo.org/modules/structs"
+ "forgejo.org/modules/util"
+ "forgejo.org/services/convert"
)
// To define the wiki related concepts:
diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go
index efcc13db99..cb984425af 100644
--- a/services/wiki/wiki_test.go
+++ b/services/wiki/wiki_test.go
@@ -8,13 +8,13 @@ import (
"strings"
"testing"
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitrepo"
+ repo_model "forgejo.org/models/repo"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ "forgejo.org/modules/git"
+ "forgejo.org/modules/gitrepo"
- _ "code.gitea.io/gitea/models/actions"
+ _ "forgejo.org/models/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -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.EqualValues(t, []string{"a/a", "b c", "d e", "f-g"}, a)
+ assert.Equal(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.EqualValues(t, test.Expected, displayName)
+ assert.Equal(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.EqualValues(t, test.Expected, WebPathToGitPath(test.WikiName))
+ assert.Equal(t, test.Expected, WebPathToGitPath(test.WikiName))
}
}
@@ -134,9 +134,9 @@ func TestUserWebGitPathConsistency(t *testing.T) {
_, userTitle1 := WebPathToUserTitle(webPath1)
gitPath1 := WebPathToGitPath(webPath1)
- 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)
+ 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)
}
}
@@ -175,7 +175,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
gitPath := WebPathToGitPath(webPath)
entry, err := masterTree.GetTreeEntryByPath(gitPath)
require.NoError(t, err)
- assert.EqualValues(t, gitPath, entry.Name(), "%s not added correctly", userTitle)
+ assert.Equal(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.EqualValues(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName)
+ assert.Equal(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.Errorf("expect to find no escaped file but we detect one")
+ t.Error("expect to find no escaped file but we detect one")
} else {
- t.Errorf("expect to find an escaped file but we could not detect one")
+ t.Error("expect to find an escaped file but we could not detect one")
}
}
- assert.EqualValues(t, tt.wikiPath, newWikiPath)
+ assert.Equal(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.EqualValues(t, "Home.md", newWikiPath)
+ assert.Equal(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.Equal(t, "", WebPathToURLPath(WebPath("")))
+ assert.Empty(t, WebPathToURLPath(WebPath("")))
}
func TestWebPathFromRequest(t *testing.T) {
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000000..a96ef516a2
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,29 @@
+{
+ 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 1ca5573cae..d4eaa29117 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -299,6 +299,13 @@
{{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"}}
@@ -397,7 +404,7 @@
{{ctx.Locale.Tr "admin.auths.update"}}
- {{ctx.Locale.Tr "admin.auths.delete"}}
+ {{ctx.Locale.Tr "admin.auths.delete"}}
@@ -414,7 +421,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 8f2b1c12e3..36d44f21f3 100644
--- a/templates/admin/config.tmpl
+++ b/templates/admin/config.tmpl
@@ -51,6 +51,16 @@
+
+
+
+ {{ctx.Locale.Tr "admin.config.global_2fa_requirement.title"}}
+ {{ctx.Locale.Tr (print "admin.config.global_2fa_requirement." .GlobalTwoFactorRequirement)}}
+
+
+
@@ -247,6 +257,16 @@
+
+
+
+ {{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 8796794aee..0a9a28fa2d 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -62,12 +62,12 @@
{{else}}
-
{{ctx.Locale.Tr "no_results_found"}}
+
{{ctx.Locale.Tr "repo.pulls.no_results"}}
{{end}}
@@ -104,7 +104,7 @@
-
diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index 4f8783dd42..08f0a4f204 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -25,7 +25,7 @@
{{svg "octicon-note" 16}}
{{else}}
- {{ctx.Locale.Tr "no_results_found"}}
+ {{ctx.Locale.Tr "repo.pulls.no_results"}}
{{end}}
{{if .Notices}}
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index b719d259e0..8c9c198897 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -67,7 +67,7 @@
{{svg "octicon-pencil"}}
{{else}}
- {{ctx.Locale.Tr "no_results_found"}}
+ {{ctx.Locale.Tr "repo.pulls.no_results"}}
{{end}}
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index f22600a449..6b6b463a6b 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -72,10 +72,10 @@
{{ctx.Locale.TrSize .CalculateBlobSize}}
{{DateUtils.AbsoluteShort .Version.CreatedUnix}}
- {{svg "octicon-trash"}}
+ {{svg "octicon-trash"}}
{{else}}
- {{ctx.Locale.Tr "no_results_found"}}
+ {{ctx.Locale.Tr "repo.pulls.no_results"}}
{{end}}
@@ -84,7 +84,7 @@
{{template "base/paginate" .}}
-
+
-
+
diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl
index afe8e6942a..57c0c210cc 100644
--- a/templates/admin/stacktrace.tmpl
+++ b/templates/admin/stacktrace.tmpl
@@ -35,7 +35,7 @@
{{end}}
-