diff options
author | Maxim Slipenko <maxim@slipenko.com> | 2024-12-19 11:45:59 +0100 |
---|---|---|
committer | Maxim Slipenko <maxim@slipenko.com> | 2024-12-19 11:45:59 +0100 |
commit | bbcd7d6fae7b806316efaae1505f0b9d9c40779c (patch) | |
tree | 0658f25815a2634aff4485780cd3f4c3ff9968a8 /routers | |
parent | fix: do not rewrite when there are no keys (diff) | |
parent | Update dependency katex to v0.16.18 (forgejo) (#6319) (diff) | |
download | forgejo-bbcd7d6fae7b806316efaae1505f0b9d9c40779c.tar.xz forgejo-bbcd7d6fae7b806316efaae1505f0b9d9c40779c.zip |
Merge remote-tracking branch 'upstream/forgejo' into feat/add-oidc-ssh-keys
Diffstat (limited to 'routers')
-rw-r--r-- | routers/api/actions/runner/main_test.go | 16 | ||||
-rw-r--r-- | routers/api/actions/runner/utils.go | 60 | ||||
-rw-r--r-- | routers/api/actions/runner/utils_test.go | 29 | ||||
-rw-r--r-- | routers/api/packages/api.go | 1 | ||||
-rw-r--r-- | routers/api/v1/admin/user.go | 38 | ||||
-rw-r--r-- | routers/api/v1/api.go | 1 | ||||
-rw-r--r-- | routers/api/v1/repo/branch.go | 71 | ||||
-rw-r--r-- | routers/api/v1/swagger/options.go | 2 | ||||
-rw-r--r-- | routers/web/auth/auth.go | 6 | ||||
-rw-r--r-- | routers/web/auth/oauth.go | 64 | ||||
-rw-r--r-- | routers/web/repo/actions/actions.go | 34 | ||||
-rw-r--r-- | routers/web/repo/actions/view.go | 18 |
12 files changed, 287 insertions, 53 deletions
diff --git a/routers/api/actions/runner/main_test.go b/routers/api/actions/runner/main_test.go new file mode 100644 index 0000000000..bed63c166e --- /dev/null +++ b/routers/api/actions/runner/main_test.go @@ -0,0 +1,16 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/forgefed" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index ff6ec5bd54..539be8d889 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -162,28 +162,56 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str return nil, fmt.Errorf("FindRunJobs: %w", err) } - ret := make(map[string]*runnerv1.TaskNeed, len(needs)) + jobIDJobs := make(map[string][]*actions_model.ActionRunJob) for _, job := range jobs { - if !needs.Contains(job.JobID) { - continue - } - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun + jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) + } + + ret := make(map[string]*runnerv1.TaskNeed, len(needs)) + for jobID, jobsWithSameID := range jobIDJobs { + if !needs.Contains(jobID) { continue } - outputs := make(map[string]string) - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) - if err != nil { - return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) + var jobOutputs map[string]string + for _, job := range jobsWithSameID { + if job.TaskID == 0 || !job.Status.IsDone() { + // it shouldn't happen, or the job has been rerun + continue + } + got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) + if err != nil { + return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) + } + outputs := make(map[string]string, len(got)) + for _, v := range got { + outputs[v.OutputKey] = v.OutputValue + } + if len(jobOutputs) == 0 { + jobOutputs = outputs + } else { + jobOutputs = mergeTwoOutputs(outputs, jobOutputs) + } } - for _, v := range got { - outputs[v.OutputKey] = v.OutputValue - } - ret[job.JobID] = &runnerv1.TaskNeed{ - Outputs: outputs, - Result: runnerv1.Result(job.Status), + ret[jobID] = &runnerv1.TaskNeed{ + Outputs: jobOutputs, + Result: runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)), } } return ret, nil } + +// mergeTwoOutputs merges two outputs from two different ActionRunJobs +// Values with the same output name may be overridden. The user should ensure the output names are unique. +// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job +func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { + ret := make(map[string]string, len(o1)) + for k1, v1 := range o1 { + if len(v1) > 0 { + ret[k1] = v1 + } else { + ret[k1] = o2[k1] + } + } + return ret +} diff --git a/routers/api/actions/runner/utils_test.go b/routers/api/actions/runner/utils_test.go new file mode 100644 index 0000000000..c8a0a28d65 --- /dev/null +++ b/routers/api/actions/runner/utils_test.go @@ -0,0 +1,29 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "context" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_findTaskNeeds(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51}) + + ret, err := findTaskNeeds(context.Background(), task) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Contains(t, ret, "job1") + assert.Len(t, ret["job1"].Outputs, 2) + assert.Equal(t, "abc", ret["job1"].Outputs["output_a"]) + assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"]) +} diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 781bfc7f90..e216a0c02b 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -337,6 +337,7 @@ func CommonRoutes() *web.Route { r.Get("/PACKAGES", cran.EnumerateSourcePackages) r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages) r.Get("/{filename}", cran.DownloadSourcePackageFile) + r.Get("/Archive/{packagename}/{filename}", cran.DownloadSourcePackageFile) }) r.Put("", reqPackageAccess(perm.AccessModeWrite), enforcePackagesQuota(), cran.UploadSourcePackageFile) }) diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index b17200381a..acab883107 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -415,6 +416,11 @@ func SearchUsers(ctx *context.APIContext) { // in: query // description: user's login name to search for // type: string + // - name: sort + // in: query + // description: sort order of results + // type: string + // enum: [oldest, newest, alphabetically, reversealphabetically, recentupdate, leastupdate] // - name: page // in: query // description: page number of results to return (1-based) @@ -431,12 +437,40 @@ func SearchUsers(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) + sort := ctx.FormString("sort") + var orderBy db.SearchOrderBy + + switch sort { + case "oldest": + orderBy = db.SearchOrderByOldest + case "newest": + orderBy = db.SearchOrderByNewest + case "alphabetically": + orderBy = db.SearchOrderByAlphabetically + case "reversealphabetically": + orderBy = db.SearchOrderByAlphabeticallyReverse + case "recentupdate": + orderBy = db.SearchOrderByRecentUpdated + case "leastupdate": + orderBy = db.SearchOrderByLeastUpdated + default: + orderBy = db.SearchOrderByAlphabetically + } + + intSource, err := strconv.ParseInt(ctx.FormString("source_id"), 10, 64) + var sourceID optional.Option[int64] + if ctx.FormString("source_id") == "" || err != nil { + sourceID = optional.None[int64]() + } else { + sourceID = optional.Some(intSource) + } + users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeIndividual, LoginName: ctx.FormTrim("login_name"), - SourceID: ctx.FormInt64("source_id"), - OrderBy: db.SearchOrderByAlphabetically, + SourceID: sourceID, + OrderBy: orderBy, ListOptions: listOptions, }) if err != nil { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index c028a5a32d..4928c9ff58 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1153,6 +1153,7 @@ func Routes() *web.Route { m.Get("/*", repo.GetBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), context.EnforceQuotaAPI(quota_model.LimitSubjectSizeGitAll, context.QuotaTargetRepo), repo.CreateBranch) + m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index e3e6efa781..3ca97f7770 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -393,6 +393,77 @@ func ListBranches(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiBranches) } +// UpdateBranch updates a repository's branch. +func UpdateBranch(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch + // --- + // summary: Update a branch + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: branch + // in: path + // description: name of the branch + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateBranchRepoOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption) + + oldName := ctx.Params("*") + repo := ctx.Repo.Repository + + if repo.IsEmpty { + ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + return + } + + if repo.IsMirror { + ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + return + } + + msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name) + if err != nil { + ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + return + } + if msg == "target_exist" { + ctx.Error(http.StatusUnprocessableEntity, "", "Cannot rename a branch using the same name or rename to a branch that already exists.") + return + } + if msg == "from_not_exist" { + ctx.Error(http.StatusNotFound, "", "Branch doesn't exist.") + return + } + + ctx.Status(http.StatusNoContent) +} + // GetBranchProtection gets a branch protection func GetBranchProtection(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 1dccf92d82..432e42d4e7 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -101,6 +101,8 @@ type swaggerParameterBodies struct { // in:body EditRepoOption api.EditRepoOption // in:body + UpdateBranchRepoOption api.UpdateBranchRepoOption + // in:body TransferRepoOption api.TransferRepoOption // in:body CreateForkOption api.CreateForkOption diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 71d7b8ca11..ccab47a9a2 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -599,8 +599,10 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. notify_service.NewUserSignUp(ctx, u) // update external user information if gothUser != nil { - if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil { - log.Error("EnsureLinkExternalToUser failed: %v", err) + if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil { + if !errors.Is(err, util.ErrNotExist) { + log.Error("UpdateExternalUser failed: %v", err) + } } } diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index e329729dcd..fbdd47479a 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -1205,39 +1205,9 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model groups := getClaimedGroups(oauth2Source, &gothUser) - opts := &user_service.UpdateOptions{} - - // Reactivate user if they are deactivated - if !u.IsActive { - opts.IsActive = optional.Some(true) - } - - // Update GroupClaims - opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) - - if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { - if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { - ctx.ServerError("SyncGroupsToTeams", err) - return - } - } - - if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil { - ctx.ServerError("EnsureLinkExternalToUser", err) - return - } - // If this user is enrolled in 2FA and this source doesn't override it, // we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page. if !needs2FA { - // Register last login - opts.SetLastLogin = true - - if err := user_service.UpdateUser(ctx, u, opts); err != nil { - ctx.ServerError("UpdateUser", err) - return - } - if err := updateSession(ctx, nil, map[string]any{ "uid": u.ID, }); err != nil { @@ -1248,6 +1218,29 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model // Clear whatever CSRF cookie has right now, force to generate a new one ctx.Csrf.DeleteCookie(ctx) + opts := &user_service.UpdateOptions{ + SetLastLogin: true, + } + opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) + if err := user_service.UpdateUser(ctx, u, opts); err != nil { + ctx.ServerError("UpdateUser", err) + return + } + + if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { + if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { + ctx.ServerError("SyncGroupsToTeams", err) + return + } + } + + // update external user information + if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil { + if !errors.Is(err, util.ErrNotExist) { + log.Error("UpdateExternalUser failed: %v", err) + } + } + if err := resetLocale(ctx, u); err != nil { ctx.ServerError("resetLocale", err) return @@ -1263,13 +1256,22 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model return } - if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() { + opts := &user_service.UpdateOptions{} + opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser) + if opts.IsAdmin.Has() || opts.IsRestricted.Has() { if err := user_service.UpdateUser(ctx, u, opts); err != nil { ctx.ServerError("UpdateUser", err) return } } + if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval { + if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil { + ctx.ServerError("SyncGroupsToTeams", err) + return + } + } + if err := updateSession(ctx, nil, map[string]any{ // User needs to use 2FA, save data and redirect to 2FA page. "twofaUid": u.ID, diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index ff3b16159b..283d476df1 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -12,11 +12,13 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -222,6 +224,10 @@ func List(ctx *context.Context) { return } + if err := loadIsRefDeleted(ctx, runs); err != nil { + log.Error("LoadIsRefDeleted", err) + } + ctx.Data["Runs"] = runs ctx.Data["Repo"] = ctx.Repo @@ -245,3 +251,31 @@ func List(ctx *context.Context) { ctx.HTML(http.StatusOK, tplListActions) } + +// loadIsRefDeleted loads the IsRefDeleted field for each run in the list. +// TODO: move this function to models/actions/run_list.go but now it will result in a circular import. +func loadIsRefDeleted(ctx *context.Context, runs actions_model.RunList) error { + branches := make(container.Set[string], len(runs)) + for _, run := range runs { + refName := git.RefName(run.Ref) + if refName.IsBranch() { + branches.Add(refName.ShortName()) + } + } + if len(branches) == 0 { + return nil + } + + branchInfos, err := git_model.GetBranches(ctx, ctx.Repo.Repository.ID, branches.Values(), false) + if err != nil { + return err + } + branchSet := git_model.BranchesToNamesSet(branchInfos) + for _, run := range runs { + refName := git.RefName(run.Ref) + if refName.IsBranch() && !branchSet.Contains(run.Ref) { + run.IsRefDeleted = true + } + } + return nil +} diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index cab37acdd1..dea31bb1c4 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -20,10 +20,13 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "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" "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/templates" @@ -157,8 +160,9 @@ type ViewUser struct { } type ViewBranch struct { - Name string `json:"name"` - Link string `json:"link"` + Name string `json:"name"` + Link string `json:"link"` + IsDeleted bool `json:"isDeleted"` } type ViewJobStep struct { @@ -227,6 +231,16 @@ func ViewPost(ctx *context_module.Context) { Name: run.PrettyRef(), Link: run.RefLink(), } + refName := git.RefName(run.Ref) + if refName.IsBranch() { + b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName()) + if err != nil && !git_model.IsErrBranchNotExist(err) { + log.Error("GetBranch: %v", err) + } else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) { + branch.IsDeleted = true + } + } + resp.State.Run.Commit = ViewCommit{ LocaleCommit: ctx.Locale.TrString("actions.runs.commit"), LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"), |