feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
@@ -6,7 +6,7 @@ package activitypub
import (
"net/http"
"code.gitea.io/gitea/services/context"
"gitea.dev/services/context"
)
func NotImplemented(ctx *context.APIContext) {
@@ -4,8 +4,8 @@
package admin
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// ListWorkflowJobs Lists all jobs
@@ -29,6 +29,14 @@ func ListWorkflowJobs(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort jobs by attribute. Supported values are "id". Default is "id"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
// type: string
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
@@ -36,8 +44,10 @@ func ListWorkflowJobs(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
shared.ListJobs(ctx, 0, 0, 0)
shared.ListJobs(ctx, 0, 0, 0, nil)
}
// ListWorkflowRuns Lists all runs
@@ -89,5 +99,5 @@ func ListWorkflowRuns(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, 0, 0)
shared.ListRuns(ctx, 0, 0, "")
}
@@ -6,12 +6,12 @@ package admin
import (
"net/http"
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/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// ListUnadoptedRepositories lists the unadopted repositories that match the provided names
@@ -6,12 +6,12 @@ package admin
import (
"net/http"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/cron"
"gitea.dev/modules/log"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/cron"
)
// ListCronTasks api for getting cron tasks
@@ -6,11 +6,11 @@ package admin
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetAllEmails
@@ -7,15 +7,15 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
"gitea.dev/models/webhook"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list system's webhooks
@@ -7,14 +7,14 @@ package admin
import (
"net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"gitea.dev/models/db"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// CreateOrg api for create organization
@@ -48,7 +48,7 @@ func CreateOrg(ctx *context.APIContext) {
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[form.Visibility]
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
@@ -67,7 +67,7 @@ func CreateOrg(ctx *context.APIContext) {
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -4,10 +4,10 @@
package admin
import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/services/context"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/repo"
"gitea.dev/services/context"
)
// CreateRepo api for creating a repository
@@ -4,8 +4,8 @@
package admin
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
@@ -9,27 +9,27 @@ import (
"fmt"
"net/http"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"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/auth/password"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/mailer"
user_service "code.gitea.io/gitea/services/user"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/auth"
"gitea.dev/models/db"
org_model "gitea.dev/models/organization"
packages_model "gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/password"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/mailer"
user_service "gitea.dev/services/user"
)
func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64) {
@@ -40,7 +40,7 @@ func parseAuthSource(ctx *context.APIContext, u *user_model.User, sourceID int64
source, err := auth.GetSourceByID(ctx, sourceID)
if err != nil {
if auth.IsErrSourceNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -97,14 +97,12 @@ func CreateUser(ctx *context.APIContext) {
if u.LoginType == auth.Plain {
if len(form.Password) < setting.MinPasswordLength {
err := errors.New("PasswordIsRequired")
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, "PasswordIsRequired")
return
}
if !password.IsComplexEnough(form.Password) {
err := errors.New("PasswordComplexity")
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, "PasswordComplexity")
return
}
@@ -112,7 +110,7 @@ func CreateUser(ctx *context.APIContext) {
if password.IsErrIsPwnedRequest(err) {
log.Error(err.Error())
}
ctx.APIError(http.StatusBadRequest, errors.New("PasswordPwned"))
ctx.APIError(http.StatusBadRequest, "PasswordPwned")
return
}
}
@@ -123,7 +121,7 @@ func CreateUser(ctx *context.APIContext) {
}
if form.Visibility != "" {
visibility := api.VisibilityModes[form.Visibility]
visibility := api.VisibilityModes[string(form.Visibility)]
overwriteDefault.Visibility = &visibility
}
@@ -143,7 +141,7 @@ func CreateUser(ctx *context.APIContext) {
user_model.IsErrEmailCharIsNotSupported(err) ||
user_model.IsErrEmailInvalid(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -196,7 +194,7 @@ func EditUser(ctx *context.APIContext) {
authOpts := &user_service.UpdateAuthOptions{
LoginSource: optional.FromNonDefault(form.SourceID),
LoginName: optional.Some(form.LoginName),
LoginName: optional.FromPtr(form.LoginName),
Password: optional.FromNonDefault(form.Password),
MustChangePassword: optional.FromPtr(form.MustChangePassword),
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
@@ -204,11 +202,11 @@ func EditUser(ctx *context.APIContext) {
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
switch {
case errors.Is(err, password.ErrMinLength):
ctx.APIError(http.StatusBadRequest, fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("password must be at least %d characters", setting.MinPasswordLength))
case errors.Is(err, password.ErrComplexity):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -222,9 +220,9 @@ func EditUser(ctx *context.APIContext) {
if !user_model.IsEmailDomainAllowed(*form.Email) {
err = fmt.Errorf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)
}
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
case user_model.IsErrEmailAlreadyUsed(err):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -239,7 +237,7 @@ func EditUser(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
@@ -249,7 +247,7 @@ func EditUser(ctx *context.APIContext) {
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
if user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -289,13 +287,13 @@ func DeleteUser(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user")
return
}
// admin should not delete themself
if ctx.ContextUser.ID == ctx.Doer.ID {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("you cannot delete yourself"))
ctx.APIError(http.StatusUnprocessableEntity, "you cannot delete yourself")
return
}
@@ -304,7 +302,7 @@ func DeleteUser(ctx *context.APIContext) {
org_model.IsErrUserHasOrgs(err) ||
packages_model.IsErrUserOwnPackages(err) ||
user_model.IsErrDeleteLastAdminUser(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -464,35 +462,18 @@ func SearchUsers(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
orderBy := db.SearchOrderByAlphabetically
sortMode := ctx.FormString("sort")
if len(sortMode) > 0 {
sortOrder := ctx.FormString("order")
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok {
if order, ok := searchModeMap[sortMode]; ok {
orderBy = order
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
return
}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
return
}
orderBy, ok := utils.ResolveSortOrder(ctx, user_model.AdminUserOrderByMap, db.SearchOrderByAlphabetically)
if !ok {
return
}
var visible []api.VisibleType
visibilityParam := ctx.FormString("visibility")
if len(visibilityParam) > 0 {
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
visible = []api.VisibleType{visibility}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam))
return
}
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
visible = []api.VisibleType{visibility}
} else if visibilityParam != "" {
ctx.APIError(http.StatusUnprocessableEntity, "invalid visibility")
return
}
searchOpts := user_model.SearchUserOptions{
@@ -566,7 +547,7 @@ func RenameUser(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
if ctx.ContextUser.IsOrganization() {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
ctx.APIError(http.StatusUnprocessableEntity, "target is an organization but not user")
return
}
@@ -575,7 +556,7 @@ func RenameUser(ctx *context.APIContext) {
// Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -6,10 +6,10 @@ package admin
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// ListUserBadges lists all badges belonging to a user
+176 -105
View File
@@ -68,36 +68,38 @@ import (
"net/http"
"strings"
auth_model "code.gitea.io/gitea/models/auth"
"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"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/activitypub"
"code.gitea.io/gitea/routers/api/v1/admin"
"code.gitea.io/gitea/routers/api/v1/misc"
"code.gitea.io/gitea/routers/api/v1/notify"
"code.gitea.io/gitea/routers/api/v1/org"
"code.gitea.io/gitea/routers/api/v1/packages"
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/api/v1/settings"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/activitypub"
"gitea.dev/routers/api/v1/admin"
"gitea.dev/routers/api/v1/misc"
"gitea.dev/routers/api/v1/notify"
"gitea.dev/routers/api/v1/org"
"gitea.dev/routers/api/v1/packages"
"gitea.dev/routers/api/v1/repo"
"gitea.dev/routers/api/v1/settings"
"gitea.dev/routers/api/v1/token"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/common"
"gitea.dev/services/actions"
"gitea.dev/services/auth"
"gitea.dev/services/context"
"gitea.dev/services/forms"
_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
_ "gitea.dev/routers/api/v1/swagger" // for swagger generation
"gitea.com/go-chi/binding"
chi_middleware "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
@@ -151,7 +153,7 @@ func repoAssignment() func(ctx *context.APIContext) {
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
context.RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetUserByName", err)
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
@@ -289,7 +291,9 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
return
}
case auth_model.AccessTokenScopeCategoryPackage:
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
// a public-only token must not reach limited-visibility owners either,
// matching the org/user public-only enforcement above
if ctx.Package != nil && !ctx.Package.Owner.Visibility.IsPublic() {
ctx.APIError(http.StatusForbidden, "token scope is limited to public packages")
return
}
@@ -414,7 +418,7 @@ func reqSiteAdmin() func(ctx *context.APIContext) {
// reqOwner user should be the owner of the repo or site admin.
func reqOwner() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.Repo.IsOwner() && !ctx.IsUserSiteAdmin() {
if !ctx.Repo.Permission.IsOwner() && !ctx.IsUserSiteAdmin() {
ctx.APIError(http.StatusForbidden, "user should be the owner of the repo")
return
}
@@ -454,7 +458,7 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) {
// reqRepoReader user should have specific read permission or be a repo admin or a site admin
func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
if !ctx.Repo.Permission.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() {
ctx.APIError(http.StatusForbidden, "user should have specific read permission or be a repo admin or a site admin")
return
}
@@ -504,41 +508,94 @@ func reqOrgOwnership() func(ctx *context.APIContext) {
}
}
// reqTeamMembership user should be an team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
// reqOrgVisible requires the organization to be visible to the doer, or a site admin
func reqOrgVisible() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.IsUserSiteAdmin() {
if ctx.Org.Organization == nil {
setting.PanicInDevOrTesting("reqOrgVisible: unprepared context")
ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context"))
return
}
if ctx.Org.Team == nil {
setting.PanicInDevOrTesting("reqTeamMembership: unprepared context")
ctx.APIErrorInternal(errors.New("reqTeamMembership: unprepared context"))
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound()
return
}
}
}
func teamAccessPrivileged(ctx *context.APIContext) (orgID int64, privileged, ok bool) {
if ctx.IsUserSiteAdmin() {
return 0, true, true
}
if ctx.Org.Team == nil {
setting.PanicInDevOrTesting("teamAccess: unprepared context")
ctx.APIErrorInternal(errors.New("teamAccess: unprepared context"))
return 0, false, false
}
orgID = ctx.Org.Team.OrgID
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return 0, false, false
} else if isOwner {
return orgID, true, true
}
isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return 0, false, false
}
return orgID, isTeamMember, true
}
func denyNonTeamMember(ctx *context.APIContext, orgID int64) {
isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
} else if isOrgMember {
ctx.APIError(http.StatusForbidden, "Must be a team member")
} else {
ctx.APIErrorNotFound()
}
}
// reqTeamReadAccess allows callers who can list the team to read its metadata.
// Non-members are admitted by the team's visibility tier and parent org visibility.
// Not sufficient for mutations — use reqOrgOwnership() or reqTeamMembership() for those.
func reqTeamReadAccess() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
orgID, privileged, ok := teamAccessPrivileged(ctx)
if !ok || privileged {
return
}
if ctx.Org.Organization == nil {
setting.PanicInDevOrTesting("reqTeamReadAccess: organization not loaded")
ctx.APIErrorInternal(errors.New("reqTeamReadAccess: organization not loaded"))
return
}
orgID := ctx.Org.Team.OrgID
isOwner, err := organization.IsOrganizationOwner(ctx, orgID, ctx.Doer.ID)
visible, err := ctx.Org.Team.CanNonMemberReadMeta(ctx, ctx.Org.Organization.AsUser(), ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if isOwner {
return
}
if !visible {
// Not admitted by visibility: 403 for org members, 404 otherwise.
denyNonTeamMember(ctx, orgID)
}
}
}
if isTeamMember, err := organization.IsTeamMember(ctx, orgID, ctx.Org.Team.ID, ctx.Doer.ID); err != nil {
ctx.APIErrorInternal(err)
return
} else if !isTeamMember {
isOrgMember, err := organization.IsOrganizationMember(ctx, orgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
} else if isOrgMember {
ctx.APIError(http.StatusForbidden, "Must be a team member")
} else {
ctx.APIErrorNotFound()
}
// reqTeamMembership user should be a team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
orgID, privileged, ok := teamAccessPrivileged(ctx)
if !ok || privileged {
return
}
denyNonTeamMember(ctx, orgID)
}
}
@@ -626,7 +683,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
if err == nil {
context.RedirectToUser(ctx.Base, ctx.Doer, ctx.PathParam("org"), redirectUserID)
} else if user_model.IsErrUserRedirectNotExist(err) {
ctx.APIErrorNotFound("GetOrgByName", err)
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
@@ -648,12 +705,23 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
}
return
}
if ctx.Org.Organization == nil {
ctx.Org.Organization, err = organization.GetOrgByID(ctx, ctx.Org.Team.OrgID)
if err != nil {
if organization.IsErrOrgNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
return
}
}
}
}
}
func mustEnableIssues(ctx *context.APIContext) {
if !ctx.Repo.CanRead(unit.TypeIssues) {
if !ctx.Repo.Permission.CanRead(unit.TypeIssues) {
if log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
@@ -676,7 +744,7 @@ func mustEnableIssues(ctx *context.APIContext) {
}
func mustAllowPulls(ctx *context.APIContext) {
if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(unit.TypePullRequests)) {
if !(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.Permission.CanRead(unit.TypePullRequests)) {
if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+
@@ -699,8 +767,8 @@ func mustAllowPulls(ctx *context.APIContext) {
}
func mustEnableIssuesOrPulls(ctx *context.APIContext) {
if !ctx.Repo.CanRead(unit.TypeIssues) &&
!(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.CanRead(unit.TypePullRequests)) {
if !ctx.Repo.Permission.CanRead(unit.TypeIssues) &&
!(ctx.Repo.Repository.CanEnablePulls() && ctx.Repo.Permission.CanRead(unit.TypePullRequests)) {
if ctx.Repo.Repository.CanEnablePulls() && log.IsTrace() {
if ctx.IsSigned {
log.Trace("Permission Denied: User %-v cannot read %-v and %-v in Repo %-v\n"+
@@ -725,7 +793,7 @@ func mustEnableIssuesOrPulls(ctx *context.APIContext) {
}
func mustEnableWiki(ctx *context.APIContext) {
if !(ctx.Repo.CanRead(unit.TypeWiki)) {
if !(ctx.Repo.Permission.CanRead(unit.TypeWiki)) {
ctx.APIErrorNotFound()
return
}
@@ -734,14 +802,14 @@ func mustEnableWiki(ctx *context.APIContext) {
// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
func mustNotBeArchived(ctx *context.APIContext) {
if ctx.Repo.Repository.IsArchived {
ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.FullName()))
ctx.APIError(http.StatusLocked, "repo is archived")
return
}
}
func mustEnableEditor(ctx *context.APIContext) {
if !ctx.Repo.Repository.CanEnableEditor() {
ctx.APIError(http.StatusLocked, fmt.Errorf("%s is not allowed to edit", ctx.Repo.Repository.FullName()))
ctx.APIError(http.StatusLocked, "repo is not allowed to edit")
return
}
}
@@ -858,19 +926,8 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
func individualPermsChecker(ctx *context.APIContext) {
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
if ctx.ContextUser.IsIndividual() {
switch ctx.ContextUser.Visibility {
case api.VisibleTypePrivate:
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
ctx.APIErrorNotFound("Visit Project", nil)
return
}
case api.VisibleTypeLimited:
if ctx.Doer == nil {
ctx.APIErrorNotFound("Visit Project", nil)
return
}
}
if ctx.ContextUser.IsIndividual() && !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.APIErrorNotFound()
}
}
@@ -885,7 +942,9 @@ func checkDeprecatedAuthMethods(ctx *context.APIContext) {
func Routes() *web.Router {
m := web.NewRouter()
m.BeforeRouting(securityHeaders())
// redirect HEAD requests to GET if no HEAD handler is defined (RFC 9110 §9.3.2)
m.BeforeRouting(chi_middleware.GetHead)
if setting.CORSConfig.Enabled {
m.BeforeRouting(cors.Handler(cors.Options{
AllowedOrigins: setting.CORSConfig.AllowDomain,
@@ -977,6 +1036,11 @@ func Routes() *web.Router {
})
})
// Token introspection and deletion endpoint
m.Combo("/token").
Get(reqToken(), token.GetCurrentToken).
Delete(reqToken(), token.DeleteCurrentToken)
// Notifications (requires 'notifications' scope)
// The notifications API is not available for public-only tokens because a user's notifications mix
// public and private repository events in the same mailbox.
@@ -1027,7 +1091,7 @@ func Routes() *web.Router {
m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos)
m.Get("/subscriptions", user.GetWatchedRepos)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Users (requires user scope)
@@ -1168,7 +1232,7 @@ func Routes() *web.Router {
m.Get("/search", repo.Search)
// (repo scope)
m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
m.Post("/migrate", reqToken(), rejectPublicOnly(), bind(api.MigrateRepoOptions{}), repo.Migrate)
m.Group("/{username}/{reponame}", func() {
m.Get("/compare/*", reqRepoReader(unit.TypeCode), repo.CompareDiff)
@@ -1189,6 +1253,7 @@ func Routes() *web.Router {
m.Group("/actions/workflows", func() {
m.Get("", repo.ActionsListRepositoryWorkflows)
m.Get("/{workflow_id}", repo.ActionsGetWorkflow)
m.Get("/{workflow_id}/runs", repo.ActionsListWorkflowRuns)
m.Put("/{workflow_id}/disable", reqRepoWriter(unit.TypeActions), repo.ActionsDisableWorkflow)
m.Put("/{workflow_id}/enable", reqRepoWriter(unit.TypeActions), repo.ActionsEnableWorkflow)
m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow)
@@ -1227,6 +1292,7 @@ func Routes() *web.Router {
})
}, reqToken())
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
m.Get("/assignees/{assignee}", reqToken(), reqAnyRepoReader(), repo.CheckRepoIssueAssignee)
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
m.Group("/teams", func() {
m.Get("", reqAnyRepoReader(), repo.ListTeams)
@@ -1278,6 +1344,10 @@ func Routes() *web.Router {
m.Group("/runs", func() {
m.Group("/{run}", func() {
m.Get("", repo.GetWorkflowRun)
m.Group("/attempts/{attempt}", func() {
m.Get("", repo.GetWorkflowRunAttempt)
m.Get("/jobs", repo.ListWorkflowRunAttemptJobs)
})
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun)
@@ -1292,7 +1362,7 @@ func Routes() *web.Router {
m.Delete("", reqRepoWriter(unit.TypeActions), repo.DeleteArtifact)
})
m.Get("/artifacts/{artifact_id}/zip", repo.DownloadArtifact)
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))
}, reqRepoReader(unit.TypeActions))
m.Group("/keys", func() {
m.Combo("").Get(repo.ListDeployKeys).
Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
@@ -1389,6 +1459,7 @@ func Routes() *web.Router {
m.Combo("/requested_reviewers", reqToken()).
Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
m.Post("/comments/{id}/replies", reqToken(), mustNotBeArchived, bind(api.CreatePullReviewCommentReplyOptions{}), repo.CreatePullReviewCommentReply)
})
m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
@@ -1513,6 +1584,10 @@ func Routes() *web.Router {
m.Combo("").Get(repo.GetIssue).
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
m.Combo("/assignees").
Post(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.AddIssueAssignees).
Delete(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.DeleteIssueAssignees)
m.Get("/assignees/{assignee}", repo.CheckIssueAssignee)
m.Group("/comments", func() {
m.Combo("").Get(repo.ListIssueComments).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
@@ -1624,21 +1699,22 @@ func Routes() *web.Router {
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll)
m.Group("/orgs/{org}", func() {
m.Combo("").Get(org.Get).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos)
m.Group("/members", func() {
m.Get("", reqToken(), org.ListMembers)
m.Combo("/{username}").Get(reqToken(), org.IsMember).
Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
})
}, reqOrgVisible())
addActionsRoutes(
m,
reqOrgMembership(),
@@ -1650,7 +1726,7 @@ func Routes() *web.Router {
m.Combo("/{username}").Get(org.IsPublicMember).
Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
})
}, reqOrgVisible())
m.Group("/teams", func() {
m.Get("", org.ListTeams)
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
@@ -1662,7 +1738,7 @@ func Routes() *web.Router {
m.Combo("/{id}").Get(reqToken(), org.GetLabel).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
})
}, reqOrgVisible())
m.Group("/hooks", func() {
m.Combo("").Get(org.ListHooks).
Post(bind(api.CreateHookOption{}), org.CreateHook)
@@ -1686,25 +1762,31 @@ func Routes() *web.Router {
}, reqToken(), reqOrgOwnership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
m.Combo("").Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam)
m.Group("", func() {
m.Get("", org.GetTeam)
m.Group("/members", func() {
m.Get("", reqOrgMembership(), org.GetTeamMembers)
m.Combo("/{username}").Get(reqOrgMembership(), org.GetTeamMember)
})
m.Group("/repos", func() {
m.Get("", org.GetTeamRepos)
m.Combo("/{org}/{reponame}").Get(org.GetTeamRepo)
})
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, reqTeamReadAccess())
m.Group("/members", func() {
m.Get("", reqToken(), org.GetTeamMembers)
m.Combo("/{username}").
Get(reqToken(), org.GetTeamMember).
Put(reqToken(), reqOrgOwnership(), org.AddTeamMember).
Delete(reqToken(), reqOrgOwnership(), org.RemoveTeamMember)
})
m.Group("/repos", func() {
m.Get("", reqToken(), org.GetTeamRepos)
m.Combo("/{org}/{reponame}").
Put(reqToken(), org.AddTeamRepository).
Delete(reqToken(), org.RemoveTeamRepository).
Get(reqToken(), org.GetTeamRepo)
Put(reqToken(), reqTeamMembership(), org.AddTeamRepository).
Delete(reqToken(), reqTeamMembership(), org.RemoveTeamRepository)
})
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), checkTokenPublicOnly())
m.Group("/admin", func() {
m.Group("/cron", func() {
@@ -1767,14 +1849,3 @@ func Routes() *web.Router {
return m
}
func securityHeaders() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
// CORB: https://www.chromium.org/Home/chromium-security/corb-for-developers
// http://stackoverflow.com/a/3146618/244009
resp.Header().Set("x-content-type-options", "nosniff")
next.ServeHTTP(resp, req)
})
}
}
@@ -6,11 +6,11 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/options"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/options"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
)
// Shows a list of all Gitignore templates
@@ -6,10 +6,10 @@ package misc
import (
"net/http"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/util"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// Shows a list of all Label templates
@@ -8,12 +8,12 @@ import (
"net/http"
"net/url"
"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/util"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/options"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
)
// Returns a list of all License templates
@@ -4,15 +4,14 @@
package misc
import (
"net/http"
"io"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/common"
"gitea.dev/services/context"
)
// Markup render markup document to HTML
@@ -36,12 +35,6 @@ func Markup(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkupOption)
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath)
}
@@ -67,12 +60,6 @@ func Markdown(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MarkdownOption)
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "")
}
@@ -98,9 +85,6 @@ func MarkdownRaw(ctx *context.APIContext) {
// "$ref": "#/responses/MarkdownRender"
// "422":
// "$ref": "#/responses/validationError"
defer ctx.Req.Body.Close()
if err := markdown.RenderRaw(markup.NewRenderContext(ctx), ctx.Req.Body, ctx.Resp); err != nil {
ctx.APIErrorInternal(err)
return
}
textBytes, _ := io.ReadAll(io.LimitReader(ctx.Req.Body, setting.UI.MaxDisplayFileSize))
common.RenderMarkup(ctx.Base, ctx.Repo, "markdown", util.UnsafeBytesToString(textBytes), "", "")
}
@@ -7,33 +7,22 @@ import (
go_context "context"
"io"
"net/http"
"os"
"path"
"strings"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/contexttest"
"gitea.dev/modules/markup"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/test"
"gitea.dev/modules/web"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
const AppURL = "http://localhost:3000/"
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{"repository.yml", "user.yml"},
})
os.Exit(m.Run())
}
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
setting.AppURL = AppURL
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
@@ -49,13 +38,11 @@ func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expe
FilePath: filePath,
}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
ctx.Repo = &context_service.Repository{}
ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
web.SetForm(ctx, &options)
Markup(ctx)
assert.Equal(t, expectedBody, resp.Body.String())
assert.Equal(t, expectedCode, resp.Code)
resp.Body.Reset()
assert.Contains(t, resp.Header().Get("Content-Security-Policy"), "script-src * 'nonce-")
}
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
@@ -76,11 +63,10 @@ func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody
Markdown(ctx)
assert.Equal(t, responseBody, resp.Body.String())
assert.Equal(t, responseCode, resp.Code)
resp.Body.Reset()
assert.Contains(t, resp.Header().Get("Content-Security-Policy"), "script-src * 'nonce-")
}
func TestAPI_RenderGFM(t *testing.T) {
unittest.PrepareTestEnv(t)
markup.Init(&markup.RenderHelperFuncs{
IsUsernameMentionable: func(ctx go_context.Context, username string) bool {
return username == "r-lyeh"
@@ -177,49 +163,34 @@ Here are some links to the most important topics. You can find the full list of
testRenderMarkup(t, "unknown", false, "", "## Test", "unsupported render mode: unknown\n", http.StatusUnprocessableEntity)
}
var simpleCases = []string{
// Guard wiki sidebar: special syntax
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
// rendered
`<p>[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]</p>
`,
// special syntax
`[[Name|Link]]`,
// rendered
`<p>[[Name|Link]]</p>
`,
// empty
``,
// rendered
``,
}
func TestAPI_RenderSimple(t *testing.T) {
setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
options := api.MarkdownOption{
Mode: "markdown",
Text: "",
Context: "/user2/repo1",
}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 {
options.Text = simpleCases[i]
web.SetForm(ctx, &options)
Markdown(ctx)
assert.Equal(t, simpleCases[i+1], resp.Body.String())
resp.Body.Reset()
}
}
func TestAPI_RenderRaw(t *testing.T) {
setting.AppURL = AppURL
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
for i := 0; i < len(simpleCases); i += 2 {
ctx.Req.Body = io.NopCloser(strings.NewReader(simpleCases[i]))
MarkdownRaw(ctx)
assert.Equal(t, simpleCases[i+1], resp.Body.String())
resp.Body.Reset()
testCases := []struct {
in, out string
mode string
}{
{in: "", out: ""},
{in: "[[special-syntax]]", out: "<p>[[special-syntax]]</p>\n", mode: "markdown"},
{in: "[[special|syntax]]", out: "<p>[[special|syntax]]</p>\n", mode: "markdown"},
{in: "01234567890123456789", out: "<p>01234567890123456789</p>\n", mode: "gfm"}, // commit-like content should not crash the render
}
t.Run("markdown", func(t *testing.T) {
for _, c := range testCases {
options := api.MarkdownOption{Mode: c.mode, Text: c.in, Context: "/user2/repo1"}
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
web.SetForm(ctx, &options)
Markdown(ctx)
assert.Equal(t, c.out, resp.Body.String())
}
})
t.Run("markdown-raw", func(t *testing.T) {
for _, c := range testCases {
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
ctx.Req.Body = io.NopCloser(strings.NewReader(c.in))
MarkdownRaw(ctx)
assert.Equal(t, c.out, resp.Body.String())
}
})
}
@@ -4,9 +4,9 @@
package misc
import (
"code.gitea.io/gitea/modules/git"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/git"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
)
func getSigningKey(ctx *context.APIContext, expectedFormat string) {
@@ -6,9 +6,9 @@ package misc
import (
"net/http"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/setting"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
)
// Version shows the version of the Gitea server
@@ -7,11 +7,11 @@ import (
"net/http"
"strings"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
)
// NewAvailable check if unread notifications exist
@@ -28,7 +28,7 @@ func NewAvailable(ctx *context.APIContext) {
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
})
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -38,7 +38,7 @@ func NewAvailable(ctx *context.APIContext) {
func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return nil
}
opts := &activities_model.FindNotificationOptions{
@@ -8,11 +8,11 @@ import (
"strings"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
func statusStringToNotificationStatus(status string) activities_model.NotificationStatus {
@@ -183,7 +183,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
if len(qLastRead) > 0 {
tmpLastRead, err := time.Parse(time.RFC3339, qLastRead)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
if !tmpLastRead.IsZero() {
@@ -7,11 +7,11 @@ import (
"fmt"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
"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"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetThread get notification by ID
@@ -104,14 +104,14 @@ func getThread(ctx *context.APIContext) *activities_model.Notification {
n, err := activities_model.GetNotificationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
return nil
}
if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID))
ctx.APIError(http.StatusForbidden, fmt.Sprintf("only user itself and admin are allowed to read/change this thread %d", n.ID))
return nil
}
return n
@@ -7,11 +7,11 @@ import (
"net/http"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListNotifications list users's notification threads
@@ -134,7 +134,7 @@ func ReadNotifications(ctx *context.APIContext) {
if len(qLastRead) > 0 {
tmpLastRead, err := time.Parse(time.RFC3339, qLastRead)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
if !tmpLastRead.IsZero() {
@@ -7,17 +7,17 @@ import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
secret_model "gitea.dev/models/secret"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/routers/api/v1/utils"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
secret_service "gitea.dev/services/secrets"
)
// ListActionsSecrets list an organization's actions secrets
@@ -111,9 +111,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -158,9 +158,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -277,7 +277,7 @@ func (Action) GetVariable(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -327,9 +327,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) {
if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -387,13 +387,13 @@ func (Action) CreateVariable(ctx *context.APIContext) {
return
}
if v != nil && v.ID > 0 {
ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
ctx.APIError(http.StatusConflict, "variable name already exists")
return
}
if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value, opt.Description); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -445,7 +445,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -462,7 +462,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -624,7 +624,7 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0)
shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0, nil)
}
func (Action) ListWorkflowRuns(ctx *context.APIContext) {
@@ -679,7 +679,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0)
shared.ListRuns(ctx, ctx.Org.Organization.ID, 0, "")
}
var _ actions_service.API = new(Action)
@@ -7,10 +7,10 @@ import (
"encoding/base64"
"net/http"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
user_service "code.gitea.io/gitea/services/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
user_service "gitea.dev/services/user"
)
// UpdateAvatarupdates the Avatar of an Organisation
@@ -39,7 +39,7 @@ func UpdateAvatar(ctx *context.APIContext) {
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
@@ -4,8 +4,8 @@
package org
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/services/context"
)
func ListBlocks(ctx *context.APIContext) {
@@ -6,11 +6,11 @@ package org
import (
"net/http"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
webhook_service "code.gitea.io/gitea/services/webhook"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list an organization's webhooks
@@ -8,13 +8,13 @@ import (
"strconv"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/label"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/label"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListLabels list all the labels of an organization
@@ -90,7 +90,7 @@ func CreateLabel(ctx *context.APIContext) {
form.Color = strings.Trim(form.Color, " ")
color, err := label.NormalizeColor(form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
form.Color = color
@@ -209,7 +209,7 @@ func EditLabel(ctx *context.APIContext) {
if form.Color != nil {
color, err := label.NormalizeColor(*form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
l.Color = color
@@ -7,15 +7,15 @@ import (
"net/http"
"net/url"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
org_service "code.gitea.io/gitea/services/org"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
org_service "gitea.dev/services/org"
)
// listMembers list an organization's members
@@ -221,7 +221,7 @@ func checkCanChangeOrgUserStatus(ctx *context.APIContext, targetUser *user_model
// allow org owners to change status of members
isOwner, err := ctx.Org.Organization.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIError(http.StatusInternalServerError, err)
ctx.APIErrorInternal(err)
} else if !isOwner {
ctx.APIError(http.StatusForbidden, "Cannot change member visibility")
}
+105 -24
View File
@@ -5,25 +5,32 @@
package org
import (
gocontext "context"
"errors"
"fmt"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
feed_service "code.gitea.io/gitea/services/feed"
"code.gitea.io/gitea/services/org"
user_service "code.gitea.io/gitea/services/user"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
repo_model "gitea.dev/models/repo"
system_model "gitea.dev/models/system"
user_model "gitea.dev/models/user"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
"gitea.dev/services/org"
repo_service "gitea.dev/services/repository"
user_service "gitea.dev/services/user"
)
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
@@ -138,8 +145,15 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
op := api.OrganizationPermissions{}
// A public-only token must not disclose membership/permission details of a
// non-public org, even for the token owner's own private orgs.
if ctx.PublicOnly && !o.Visibility.IsPublic() {
ctx.APIErrorNotFound()
return
}
if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
ctx.APIErrorNotFound()
return
}
@@ -249,13 +263,13 @@ func Create(ctx *context.APIContext) {
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateOrgOption)
if !ctx.Doer.CanCreateOrganization() {
ctx.APIError(http.StatusForbidden, nil)
ctx.APIError(http.StatusForbidden, "not allowed to create org")
return
}
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[form.Visibility]
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
@@ -275,7 +289,7 @@ func Create(ctx *context.APIContext) {
db.IsErrNameReserved(err) ||
db.IsErrNameCharsNotAllowed(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -305,7 +319,7 @@ func Get(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
ctx.APIErrorNotFound()
return
}
@@ -348,7 +362,7 @@ func Rename(ctx *context.APIContext) {
orgUser := ctx.Org.Organization.AsUser()
if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -387,7 +401,7 @@ func Edit(ctx *context.APIContext) {
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -399,7 +413,7 @@ func Edit(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
@@ -502,3 +516,70 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
func deleteOrgReposBackground(ctx gocontext.Context, org *organization.Organization, repoIDs []int64, doer *user_model.User) {
defer func() {
if r := recover(); r != nil {
log.Error("panic during org repo deletion: %v, stack: %v", r, log.Stack(2))
}
}()
for _, repoID := range repoIDs {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
desc := fmt.Sprintf("Failed to get repository ID %d in org %s: %v", repoID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("GetRepositoryByID failed: %v", desc)
continue
}
if err := repo_service.DeleteRepository(ctx, doer, repo, true); err != nil {
desc := fmt.Sprintf("Failed to delete repository %s (ID: %d) in org %s: %v", repo.Name, repo.ID, org.Name, err)
_ = system_model.CreateNotice(ctx, system_model.NoticeRepository, desc)
log.Error("DeleteRepository failed: %v", desc)
continue
}
log.Info("Successfully deleted repository %s (ID: %d) in org %s", repo.Name, repo.ID, org.Name)
}
log.Info("Completed deletion of repositories in org %s", org.Name)
}
func DeleteOrgRepos(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/repos organization orgDeleteRepos
// ---
// summary: Delete all repositories in an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "202":
// "$ref": "#/responses/empty"
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// Intentionally it only loads repository IDs to avoid loading too much data into memory
// There is no need to do pagination here as the number of repositories is expected to be manageable
repoIDs, err := repo_model.GetOrgRepositoryIDs(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if len(repoIDs) == 0 {
ctx.Status(http.StatusNoContent)
return
}
// Start deletion (slow) in background with detached context, so it can continue even if the request is canceled
go deleteOrgReposBackground(graceful.GetManager().ShutdownContext(), ctx.Org.Organization, repoIDs, ctx.Doer)
ctx.Status(http.StatusAccepted)
}
+96 -44
View File
@@ -8,23 +8,23 @@ import (
"errors"
"net/http"
activities_model "code.gitea.io/gitea/models/activities"
"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"
unit_model "code.gitea.io/gitea/models/unit"
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/web"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
feed_service "code.gitea.io/gitea/services/feed"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/user"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
org_service "gitea.dev/services/org"
repo_service "gitea.dev/services/repository"
)
// ListTeams list all the teams of an organization
@@ -55,10 +55,15 @@ func ListTeams(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
listOptions := utils.GetListOptions(ctx)
teams, count, err := organization.SearchTeam(ctx, &organization.SearchTeamOptions{
opts := &organization.SearchTeamOptions{
ListOptions: listOptions,
OrgID: ctx.Org.Organization.ID,
})
}
if err := organization.ApplyTeamListFilter(ctx, ctx.Org.Organization.ID, ctx.Doer, ctx.IsSigned, opts); err != nil {
ctx.APIErrorInternal(err)
return
}
teams, count, err := organization.SearchTeam(ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -210,7 +215,7 @@ func CreateTeam(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateTeamOption)
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
team := &organization.Team{
OrgID: ctx.Org.Organization.ID,
Name: form.Name,
@@ -218,13 +223,14 @@ func CreateTeam(ctx *context.APIContext) {
IncludesAllRepositories: form.IncludesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
AccessMode: teamPermission,
Visibility: organization.NormalizeTeamVisibility(string(form.Visibility)),
}
if team.AccessMode < perm.AccessModeAdmin {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
} else {
ctx.APIErrorInternal(errors.New("units permission should not be empty"))
@@ -236,7 +242,7 @@ func CreateTeam(ctx *context.APIContext) {
if err := org_service.NewTeam(ctx, team); err != nil {
if organization.IsErrTeamAlreadyExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -295,10 +301,14 @@ func EditTeam(ctx *context.APIContext) {
team.Description = *form.Description
}
if form.Visibility != nil && !team.IsOwnerTeam() {
team.Visibility = organization.NormalizeTeamVisibility(string(*form.Visibility))
}
isAuthChanged := false
isIncludeAllChanged := false
if !team.IsOwnerTeam() && len(form.Permission) != 0 {
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
if team.AccessMode != teamPermission {
isAuthChanged = true
team.AccessMode = teamPermission
@@ -314,7 +324,7 @@ func EditTeam(ctx *context.APIContext) {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
}
} else {
@@ -387,15 +397,6 @@ func GetTeamMembers(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
isMember, err := organization.IsOrganizationMember(ctx, ctx.Org.Team.OrgID, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
} else if !isMember && !ctx.Doer.IsAdmin {
ctx.APIErrorNotFound()
return
}
listOptions := utils.GetListOptions(ctx)
teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
ListOptions: listOptions,
@@ -490,7 +491,7 @@ func AddTeamMember(ctx *context.APIContext) {
}
if err := org_service.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -566,25 +567,43 @@ func GetTeamRepos(ctx *context.APIContext) {
team := ctx.Org.Team
listOptions := utils.GetListOptions(ctx)
teamRepos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
// A public-only token must not expose (or count) private repos, even when the
// doer owning the token otherwise has access to them, so filter them out at the
// query level to keep the returned page and the total-count header consistent.
searchOpts := &repo_model.SearchTeamRepoOptions{
ListOptions: listOptions,
TeamID: team.ID,
})
PublicOnly: ctx.PublicOnly,
}
teamRepos, err := repo_model.GetTeamRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos := make([]*api.Repository, len(teamRepos))
for i, repo := range teamRepos {
count, err := repo_model.CountTeamRepositories(ctx, searchOpts)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos := make([]*api.Repository, 0, len(teamRepos))
for _, repo := range teamRepos {
permission, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
repos[i] = convert.ToRepo(ctx, repo, permission)
// A team's repo list is reachable by non-team-members through the team's
// visibility tier, so never expose repos (incl. their names) the doer
// cannot access. This per-repo visibility trim can't be expressed in the
// SQL count above without regressing per-unit public access, so for such
// non-members the total-count header may be a small upper bound.
if !permission.HasAnyUnitAccessOrPublicAccess() {
continue
}
repos = append(repos, convert.ToRepo(ctx, repo, permission))
}
ctx.SetLinkHeader(int64(team.NumRepos), listOptions.PageSize)
ctx.SetTotalCountHeader(int64(team.NumRepos))
ctx.SetLinkHeader(count, listOptions.PageSize)
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, repos)
}
@@ -623,6 +642,12 @@ func GetTeamRepo(ctx *context.APIContext) {
return
}
// A public-only token must not confirm the existence of a private repo.
if !ctx.TokenCanAccessRepo(repo) {
ctx.APIErrorNotFound()
return
}
if !organization.HasTeamRepo(ctx, ctx.Org.Team.OrgID, ctx.Org.Team.ID, repo.ID) {
ctx.APIErrorNotFound()
return
@@ -633,6 +658,12 @@ func GetTeamRepo(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
// The team may be reachable by a non-team-member via its visibility tier;
// don't confirm the existence of a repo the doer cannot access.
if !permission.HasAnyUnitAccessOrPublicAccess() {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission))
}
@@ -651,6 +682,19 @@ func getRepositoryByParams(ctx *context.APIContext) *repo_model.Repository {
return repo
}
func canChangeTeamRepository(ctx *context.APIContext) bool {
canChange, err := ctx.Org.Organization.CanChangeRepoTeamAccess(ctx, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return false
}
if !canChange {
ctx.APIError(http.StatusForbidden, "Must be an organization owner")
return false
}
return true
}
// AddTeamRepository api for adding a repository to a team
func AddTeamRepository(ctx *context.APIContext) {
// swagger:operation PUT /teams/{id}/repos/{org}/{repo} organization orgAddTeamRepository
@@ -687,6 +731,9 @@ func AddTeamRepository(ctx *context.APIContext) {
if ctx.Written() {
return
}
if !canChangeTeamRepository(ctx) {
return
}
if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.APIErrorInternal(err)
return
@@ -739,6 +786,9 @@ func RemoveTeamRepository(ctx *context.APIContext) {
if ctx.Written() {
return
}
if !canChangeTeamRepository(ctx) {
return
}
if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
ctx.APIErrorInternal(err)
return
@@ -806,9 +856,9 @@ func SearchTeam(ctx *context.APIContext) {
ListOptions: listOptions,
}
// Only admin is allowed to search for all teams
if !ctx.Doer.IsAdmin {
opts.UserID = ctx.Doer.ID
if err := organization.ApplyTeamListFilter(ctx, ctx.Org.Organization.ID, ctx.Doer, ctx.IsSigned, opts); err != nil {
ctx.APIErrorInternal(err)
return
}
teams, maxResults, err := organization.SearchTeam(ctx, opts)
@@ -876,6 +926,8 @@ func ListTeamActivityFeeds(ctx *context.APIContext) {
Date: ctx.FormString("date"),
ListOptions: listOptions,
}
// A public-only token must not receive private activity entries.
opts.ApplyPublicOnly(ctx.PublicOnly)
feeds, count, err := feed_service.GetFeeds(ctx, opts)
if err != nil {
@@ -7,15 +7,15 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
"gitea.dev/models/packages"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
packages_service "gitea.dev/services/packages"
)
// ListPackages gets all packages of an owner
@@ -329,7 +329,7 @@ func GetLatestPackageVersion(ctx *context.APIContext) {
return
}
if len(pvs) == 0 {
ctx.APIError(http.StatusNotFound, err)
ctx.APIErrorNotFound()
return
}
@@ -383,7 +383,7 @@ func LinkPackage(ctx *context.APIContext) {
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -393,7 +393,7 @@ func LinkPackage(ctx *context.APIContext) {
repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -404,9 +404,9 @@ func LinkPackage(ctx *context.APIContext) {
if err != nil {
switch {
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -445,7 +445,7 @@ func UnlinkPackage(ctx *context.APIContext) {
pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -456,9 +456,9 @@ func UnlinkPackage(ctx *context.APIContext) {
if err != nil {
switch {
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -16,23 +16,24 @@ import (
"strings"
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/httplib"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
secret_service "code.gitea.io/gitea/services/secrets"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
secret_model "gitea.dev/models/secret"
"gitea.dev/modules/actions"
"gitea.dev/modules/httplib"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/shared"
"gitea.dev/routers/api/v1/utils"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
"gitea.dev/services/convert"
secret_service "gitea.dev/services/secrets"
"github.com/nektos/act/pkg/model"
"gitea.com/gitea/runner/act/model"
)
// ListActionsSecrets list an repo's actions secrets
@@ -140,9 +141,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.PathParam("secretname"), opt.Data, opt.Description)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -194,9 +195,9 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.PathParam("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -242,7 +243,7 @@ func (Action) GetVariable(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -297,9 +298,9 @@ func (Action) DeleteVariable(ctx *context.APIContext) {
if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParam("variablename")); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -360,13 +361,13 @@ func (Action) CreateVariable(ctx *context.APIContext) {
return
}
if v != nil && v.ID > 0 {
ctx.APIError(http.StatusConflict, util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
ctx.APIError(http.StatusConflict, "variable name already exists")
return
}
if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value, opt.Description); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -421,7 +422,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -438,7 +439,7 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -674,7 +675,7 @@ func (Action) UpdateRunner(ctx *context.APIContext) {
shared.UpdateRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id"))
}
// GetWorkflowRunJobs Lists all jobs for a workflow run.
// ListWorkflowJobs Lists all jobs for a repository.
func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs
// ---
@@ -705,6 +706,14 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort jobs by attribute. Supported values are "id". Default is "id"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
// type: string
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
@@ -712,10 +721,12 @@ func (Action) ListWorkflowJobs(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
repoID := ctx.Repo.Repository.ID
shared.ListJobs(ctx, 0, repoID, 0)
shared.ListJobs(ctx, 0, repoID, 0, nil)
}
// ListWorkflowRuns Lists all runs for a repository run.
@@ -761,6 +772,11 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: exclude_pull_requests
// in: query
// description: if true, the `pull_requests` field on each returned run is emptied
// type: boolean
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
@@ -779,7 +795,7 @@ func (Action) ListWorkflowRuns(ctx *context.APIContext) {
repoID := ctx.Repo.Repository.ID
shared.ListRuns(ctx, 0, repoID)
shared.ListRuns(ctx, 0, repoID, "")
}
var _ actions_service.API = new(Action)
@@ -845,6 +861,12 @@ func ListActionTasks(ctx *context.APIContext) {
res := new(api.ActionTaskResponse)
res.TotalCount = total
taskList := actions_model.TaskList(tasks)
if err := taskList.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries = make([]*api.ActionTask, len(tasks))
for i := range tasks {
convertedTask, err := convert.ToActionTask(ctx, tasks[i])
@@ -856,7 +878,7 @@ func ListActionTasks(ctx *context.APIContext) {
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total) // Duplicates api response field but it's better to set it for consistency
ctx.SetTotalCountHeader(total) // Duplicates api response field, but it's better to set it for consistency
ctx.JSON(http.StatusOK, &res)
}
@@ -940,7 +962,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -950,6 +972,107 @@ func ActionsGetWorkflow(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, workflow)
}
func ActionsListWorkflowRuns(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs repository ActionsListWorkflowRuns
// ---
// summary: List runs for a workflow
// 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: workflow_id
// in: path
// description: id of the workflow, must be the workflow file name (e.g. `build.yml`)
// type: string
// required: true
// - name: event
// in: query
// description: workflow event name
// type: string
// required: false
// - name: branch
// in: query
// description: workflow branch
// type: string
// required: false
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: actor
// in: query
// description: triggered by user
// type: string
// required: false
// - name: head_sha
// in: query
// description: triggering sha of the workflow run
// type: string
// required: false
// - name: exclude_pull_requests
// in: query
// description: if true, the `pull_requests` field on each returned run is emptied
// type: boolean
// required: false
// - name: scoped_workflow_source_repo_id
// description: For a scoped workflow, the ID of the source repository providing it; omit or 0 for a repo-level workflow.
// in: query
// type: integer
// format: int64
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowRunsList"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
workflowID := ctx.PathParam("workflow_id")
scopedWorkflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id")
// Existing runs prove the workflow is/was valid and cover historical workflows whose file was later removed.
// Repo-level never-run workflows fall back to a git lookup; scoped workflows are selected by source repo ID and may return an empty run list.
runExists, err := db.Exist[actions_model.ActionRun](ctx, actions_model.FindRunOptions{
RepoID: ctx.Repo.Repository.ID,
WorkflowID: workflowID,
WorkflowRepoID: scopedWorkflowSourceRepoID,
IsScopedRun: optional.Some(scopedWorkflowSourceRepoID > 0),
}.ToConds())
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !runExists {
if scopedWorkflowSourceRepoID == 0 {
if _, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID); err != nil {
ctx.APIErrorAuto(err)
return
}
}
}
shared.ListRuns(ctx, 0, ctx.Repo.Repository.ID, workflowID)
}
func ActionsDisableWorkflow(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow
// ---
@@ -988,7 +1111,7 @@ func ActionsDisableWorkflow(ctx *context.APIContext) {
err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -1028,6 +1151,11 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
// description: Whether the response should include the workflow run ID and URLs.
// in: query
// type: boolean
// - name: scoped_workflow_source_repo_id
// description: For a scoped workflow, the ID of the source repository providing it; omit or 0 for a repo-level workflow.
// in: query
// type: integer
// format: int64
// responses:
// "200":
// "$ref": "#/responses/RunDetails"
@@ -1045,11 +1173,13 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
workflowID := ctx.PathParam("workflow_id")
opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch)
if opt.Ref == "" {
ctx.APIError(http.StatusUnprocessableEntity, util.NewInvalidArgumentErrorf("ref is required parameter"))
ctx.APIError(http.StatusUnprocessableEntity, "ref is required parameter")
return
}
runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
// a non-zero scoped_workflow_source_repo_id dispatches a scoped workflow from that source repo; 0/absent is repo-level.
scopedWorkflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id")
runID, err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, scopedWorkflowSourceRepoID, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
// The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
// So we have to manually read the `inputs[key]` from the form
@@ -1071,9 +1201,11 @@ func ActionsDispatchWorkflow(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -1132,7 +1264,7 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -1145,13 +1277,11 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun {
runID := ctx.PathParamInt64("run")
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
return nil
} else if err != nil {
ctx.APIErrorInternal(err)
if err != nil {
ctx.APIErrorAuto(err)
return nil
}
run.Repo = ctx.Repo.Repository
return run
}
@@ -1161,14 +1291,30 @@ func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.Ac
return nil, nil
}
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
}
jobs.SortMatrixGroupsByName()
return run, jobs
}
func getCurrentRepoActionRunAttemptByNumber(ctx *context.APIContext) (*actions_model.ActionRun, *actions_model.ActionRunAttempt) {
run := getCurrentRepoActionRunByID(ctx)
if ctx.Written() {
return nil, nil
}
attemptNum := ctx.PathParamInt64("attempt")
attempt, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(ctx, run.ID, attemptNum)
if err != nil {
ctx.APIErrorAuto(err)
return nil, nil
}
return run, attempt
}
// GetWorkflowRun Gets a specific workflow run.
func GetWorkflowRun(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
@@ -1205,7 +1351,56 @@ func GetWorkflowRun(ctx *context.APIContext) {
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convertedRun)
}
// GetWorkflowRunAttempt Gets a specific workflow run attempt.
func GetWorkflowRunAttempt(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt} repository getWorkflowRunAttempt
// ---
// summary: Gets a specific workflow run attempt
// 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 repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the run
// type: integer
// required: true
// - name: attempt
// in: path
// description: logical attempt number of the run
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/WorkflowRun"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
run, attempt := getCurrentRepoActionRunAttemptByNumber(ctx)
if ctx.Written() {
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, attempt, false)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1245,6 +1440,8 @@ func RerunWorkflowRun(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
@@ -1253,12 +1450,12 @@ func RerunWorkflowRun(ctx *context.APIContext) {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil {
if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, jobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, false)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1296,6 +1493,8 @@ func RerunFailedWorkflowRun(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
@@ -1304,7 +1503,14 @@ func RerunFailedWorkflowRun(ctx *context.APIContext) {
return
}
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
failedJobs := actions_service.GetFailedJobsForRerun(jobs)
// Empty failedJobs means no failed jobs to re-run
if len(failedJobs) == 0 {
ctx.APIError(http.StatusBadRequest, "this workflow run has no failed jobs to re-run")
return
}
if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, failedJobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}
@@ -1349,6 +1555,8 @@ func RerunWorkflowJob(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "409":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
@@ -1360,17 +1568,33 @@ func RerunWorkflowJob(ctx *context.APIContext) {
jobID := ctx.PathParamInt64("job_id")
jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID })
if jobIdx == -1 {
ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID))
ctx.APIErrorNotFound("workflow job not found")
return
}
targetJob := jobs[jobIdx]
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetAllRerunJobs(targetJob, jobs)); err != nil {
newAttempt, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, []*actions_model.ActionRunJob{targetJob})
if err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob)
// Legacy jobs had AttemptJobID=0 before the rerun; createOriginalAttemptForLegacyRun inside
// RerunWorkflowRunJobs has since backfilled it in the DB, so reload only in that case.
if targetJob.AttemptJobID == 0 {
targetJob, err = actions_model.GetRunJobByRepoAndID(ctx, run.RepoID, targetJob.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
rerunJob, err := actions_model.GetRunJobByAttemptJobID(ctx, run.ID, newAttempt.ID, targetJob.AttemptJobID)
if err != nil {
handleWorkflowRerunError(ctx, err)
return
}
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, rerunJob)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -1380,7 +1604,13 @@ func RerunWorkflowJob(ctx *context.APIContext) {
func handleWorkflowRerunError(ctx *context.APIContext, err error) {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
} else if errors.Is(err, util.ErrAlreadyExist) {
ctx.APIError(http.StatusConflict, err.Error())
return
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1422,6 +1652,83 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
// - name: sort
// in: query
// description: sort jobs by attribute. Supported values are "id". Default is "id"
// type: string
// - name: order
// in: query
// description: sort order, either "asc" (ascending) or "desc" (descending). Default is "asc"
// type: string
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
if runID <= 0 {
ctx.APIError(http.StatusBadRequest, "runID must be a positive integer")
return
}
run, err := actions_model.GetRunByRepoAndID(ctx, repoID, runID)
if err != nil {
ctx.APIErrorAuto(err)
return
}
// runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID.
// no additional checks for runID are needed here
shared.ListJobs(ctx, 0, repoID, runID, optional.Some(run.LatestAttemptID))
}
// ListWorkflowRunAttemptJobs Lists all jobs for a workflow run attempt.
func ListWorkflowRunAttemptJobs(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/attempts/{attempt}/jobs repository listWorkflowRunAttemptJobs
// ---
// summary: Lists all jobs for a workflow run attempt
// 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 repository
// type: string
// required: true
// - name: run
// in: path
// description: id of the workflow run
// type: integer
// required: true
// - name: attempt
// in: path
// description: logical attempt number of the run
// type: integer
// required: true
// - name: status
// in: query
// description: workflow status (pending, queued, in_progress, failure, success, skipped)
// type: string
// required: false
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WorkflowJobsList"
@@ -1430,17 +1737,12 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
if runID <= 0 {
ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer"))
run, attempt := getCurrentRepoActionRunAttemptByNumber(ctx)
if ctx.Written() {
return
}
// runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID.
// no additional checks for runID are needed here
shared.ListJobs(ctx, 0, repoID, runID)
shared.ListJobs(ctx, 0, run.RepoID, run.ID, optional.Some(attempt.ID))
}
// GetWorkflowJob Gets a specific workflow job for a workflow run.
@@ -1482,7 +1784,7 @@ func GetWorkflowJob(ctx *context.APIContext) {
}
if !has || job.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound(util.ErrNotExist)
ctx.APIErrorNotFound()
return
}
@@ -1706,7 +2008,7 @@ func GetArtifact(ctx *context.APIContext) {
return
}
if actions.IsArtifactV4(art) {
if actions_service.IsArtifactV4(art) {
convertedArtifact, err := convert.ToActionArtifact(ctx.Repo.Repository, art)
if err != nil {
ctx.APIErrorInternal(err)
@@ -1755,8 +2057,8 @@ func DeleteArtifact(ctx *context.APIContext) {
return
}
if actions.IsArtifactV4(art) {
if err := actions_model.SetArtifactNeedDelete(ctx, art.RunID, art.ArtifactName); err != nil {
if actions_service.IsArtifactV4(art) {
if err := actions_model.SetArtifactNeedDeleteByID(ctx, art.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
@@ -1771,8 +2073,8 @@ func buildSignature(endp string, expires, artifactID int64) []byte {
return actions.BuildSignature("api", endp, strconv.FormatInt(expires, 10), strconv.FormatInt(artifactID, 10))
}
func buildDownloadRawEndpoint(repo *repo_model.Repository, artifactID int64) string {
return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), artifactID)
func buildDownloadRawEndpoint(ownerName, repoName string, artifactID int64) string {
return fmt.Sprintf("api/v1/repos/%s/%s/actions/artifacts/%d/zip/raw", url.PathEscape(ownerName), url.PathEscape(repoName), artifactID)
}
func buildSigURL(ctx go_context.Context, endPoint string, artifactID int64) string {
@@ -1824,16 +2126,16 @@ func DownloadArtifact(ctx *context.APIContext) {
return
}
if actions.IsArtifactV4(art) {
if actions_service.IsArtifactV4(art) {
// @actions/toolkit asserts that downloaded artifacts of a different runid return 302
// https://github.com/actions/toolkit/blob/44d43b5490b02998bd09b0c4ff369a4cc67876c2/packages/artifact/src/internal/download/download-artifact.ts#L203-L210
if actions.DownloadArtifactV4ServeDirect(ctx.Base, art) {
if actions_service.DownloadArtifactV4ServeDirect(ctx.Base, art) {
return
}
// @actions/toolkit asserts a 302 for the artifact download, so we have to build a signed URL and redirect to it
// TODO: a perma link to the code for reference
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository, art.ID), art.ID)
redirectURL := buildSigURL(ctx, buildDownloadRawEndpoint(ctx.Repo.Repository.OwnerName, ctx.Repo.Repository.Name, art.ID), art.ID)
ctx.Redirect(redirectURL, http.StatusFound)
return
}
@@ -1844,7 +2146,22 @@ func DownloadArtifact(ctx *context.APIContext) {
// DownloadArtifactRaw Downloads a specific artifact for a workflow run directly.
func DownloadArtifactRaw(ctx *context.APIContext) {
// it doesn't use repoAssignment middleware, so it needs to prepare the repo and check permission (sig) by itself
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ctx.PathParam("username"), ctx.PathParam("reponame"))
ownerName, repoName := ctx.PathParam("username"), ctx.PathParam("reponame")
query := ctx.Req.URL.Query()
sigBytes, _ := base64.RawURLEncoding.DecodeString(query.Get("sig"))
expires, _ := strconv.ParseInt(query.Get("expires"), 10, 64)
artifactID := ctx.PathParamInt64("artifact_id")
if !hmac.Equal(sigBytes, buildSignature(buildDownloadRawEndpoint(ownerName, repoName, artifactID), expires, artifactID)) {
ctx.APIErrorNotFound()
return
}
if time.Unix(expires, 0).Before(time.Now()) {
ctx.APIError(http.StatusUnauthorized, "Error link expired")
return
}
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound()
@@ -1858,29 +2175,13 @@ func DownloadArtifactRaw(ctx *context.APIContext) {
return
}
sigStr := ctx.Req.URL.Query().Get("sig")
expiresStr := ctx.Req.URL.Query().Get("expires")
sigBytes, _ := base64.RawURLEncoding.DecodeString(sigStr)
expires, _ := strconv.ParseInt(expiresStr, 10, 64)
expectedSig := buildSignature(buildDownloadRawEndpoint(repo, art.ID), expires, art.ID)
if !hmac.Equal(sigBytes, expectedSig) {
ctx.APIError(http.StatusUnauthorized, "Error unauthorized")
return
}
t := time.Unix(expires, 0)
if t.Before(time.Now()) {
ctx.APIError(http.StatusUnauthorized, "Error link expired")
return
}
// if artifacts status is not uploaded-confirmed, treat it as not found
if art.Status == actions_model.ArtifactStatusExpired {
ctx.APIError(http.StatusNotFound, "Artifact has expired")
return
}
if actions.IsArtifactV4(art) {
err := actions.DownloadArtifactV4(ctx.Base, art)
if actions_service.IsArtifactV4(art) {
err := actions_service.DownloadArtifactV4(ctx.Base, art)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -4,12 +4,9 @@
package repo
import (
"errors"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
actions_model "gitea.dev/models/actions"
"gitea.dev/routers/common"
"gitea.dev/services/context"
)
func DownloadActionsRunJobLogs(ctx *context.APIContext) {
@@ -45,11 +42,7 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
jobID := ctx.PathParamInt64("job_id")
curJob, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if err = curJob.LoadRepo(ctx); err != nil {
@@ -59,10 +52,6 @@ func DownloadActionsRunJobLogs(ctx *context.APIContext) {
err = common.DownloadActionsRunJobLogs(ctx.Base, ctx.Repo.Repository, curJob)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
}
}
@@ -7,10 +7,10 @@ import (
"encoding/base64"
"net/http"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
repo_service "gitea.dev/services/repository"
)
// UpdateVatar updates the Avatar of an Repo
@@ -44,7 +44,7 @@ func UpdateAvatar(ctx *context.APIContext) {
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
@@ -6,8 +6,8 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
"gitea.dev/services/context"
files_service "gitea.dev/services/repository/files"
)
// GetBlob get the blob of a repository file.
@@ -48,7 +48,7 @@ func GetBlob(ctx *context.APIContext) {
}
if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.JSON(http.StatusOK, blob)
}
@@ -8,24 +8,24 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
"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/gitrepo"
"code.gitea.io/gitea/modules/optional"
repo_module "code.gitea.io/gitea/modules/repository"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
pull_service "code.gitea.io/gitea/services/pull"
release_service "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/optional"
repo_module "gitea.dev/modules/repository"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
pull_service "gitea.dev/services/pull"
release_service "gitea.dev/services/release"
repo_service "gitea.dev/services/repository"
)
// GetBranch get a branch of a repository
@@ -64,7 +64,7 @@ func GetBranch(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
} else if !exist {
ctx.APIErrorNotFound(err)
ctx.APIErrorNotFound()
return
}
@@ -80,7 +80,7 @@ func GetBranch(ctx *context.APIContext) {
return
}
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branchName, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branchName, c, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin())
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -153,11 +153,11 @@ func DeleteBranch(ctx *context.APIContext) {
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
ctx.APIErrorNotFound()
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.APIError(http.StatusForbidden, errors.New("can not delete default or pull request target branch"))
ctx.APIError(http.StatusForbidden, "can not delete default or pull request target branch")
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.APIError(http.StatusForbidden, errors.New("branch protected"))
ctx.APIError(http.StatusForbidden, "branch protected")
default:
ctx.APIErrorInternal(err)
}
@@ -271,7 +271,7 @@ func CreateBranch(ctx *context.APIContext) {
return
}
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, opt.BranchName, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, opt.BranchName, commit, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin())
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -306,6 +306,10 @@ func ListBranches(ctx *context.APIContext) {
// in: query
// description: page size of results
// type: integer
// - name: q
// in: query
// description: branch name substring to filter by
// type: string
// responses:
// "200":
// "$ref": "#/responses/BranchList"
@@ -314,6 +318,7 @@ func ListBranches(ctx *context.APIContext) {
var apiBranches []*api.Branch
listOptions := utils.GetListOptions(ctx)
keyword := ctx.FormString("q")
if !ctx.Repo.Repository.IsEmpty {
if ctx.Repo.GitRepo == nil {
@@ -325,6 +330,7 @@ func ListBranches(ctx *context.APIContext) {
ListOptions: listOptions,
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: optional.Some(false),
Keyword: keyword,
}
var err error
totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
@@ -366,7 +372,7 @@ func ListBranches(ctx *context.APIContext) {
}
branchProtection := rules.GetFirstMatched(branches[i].Name)
apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.Permission.IsAdmin())
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -440,9 +446,9 @@ func UpdateBranch(ctx *context.APIContext) {
if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.APIErrorNotFound(err)
ctx.APIErrorNotFound()
case errors.Is(err, util.ErrInvalidArgument):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case git.IsErrPushRejected(err):
rej := err.(*git.ErrPushRejected)
ctx.APIError(http.StatusForbidden, rej.Message)
@@ -678,7 +684,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -687,7 +693,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
forcePushAllowlistUsers, err := user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -696,7 +702,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -705,18 +711,30 @@ func CreateBranchProtection(ctx *context.APIContext) {
approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
var bypassAllowlistUsers []int64
if form.EnableBypassAllowlist {
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -725,7 +743,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -734,7 +752,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -743,12 +761,23 @@ func CreateBranchProtection(ctx *context.APIContext) {
approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
if form.EnableBypassAllowlist {
bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
}
}
protectBranch = &git_model.ProtectedBranch{
@@ -762,6 +791,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
EnableForcePushAllowlist: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist,
ForcePushAllowlistDeployKeys: form.EnablePush && form.EnableForcePush && form.EnableForcePushAllowlist && form.ForcePushAllowlistDeployKeys,
EnableMergeWhitelist: form.EnableMergeWhitelist,
EnableBypassAllowlist: form.EnableBypassAllowlist,
EnableStatusCheck: form.EnableStatusCheck,
StatusCheckContexts: form.StatusCheckContexts,
EnableApprovalsWhitelist: form.EnableApprovalsWhitelist,
@@ -786,6 +816,8 @@ func CreateBranchProtection(ctx *context.APIContext) {
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
BypassUserIDs: bypassAllowlistUsers,
BypassTeamIDs: bypassAllowlistTeams,
}); err != nil {
ctx.APIErrorInternal(err)
return
@@ -906,6 +938,10 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
}
if form.EnableBypassAllowlist != nil {
protectBranch.EnableBypassAllowlist = *form.EnableBypassAllowlist
}
if form.EnableStatusCheck != nil {
protectBranch.EnableStatusCheck = *form.EnableStatusCheck
}
@@ -958,12 +994,12 @@ func EditBranchProtection(ctx *context.APIContext) {
protectBranch.BlockAdminMergeOverride = *form.BlockAdminMergeOverride
}
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers []int64
var whitelistUsers, forcePushAllowlistUsers, mergeWhitelistUsers, approvalsWhitelistUsers, bypassAllowlistUsers []int64
if form.PushWhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -976,7 +1012,7 @@ func EditBranchProtection(ctx *context.APIContext) {
forcePushAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.ForcePushAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -989,7 +1025,7 @@ func EditBranchProtection(ctx *context.APIContext) {
mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1002,7 +1038,7 @@ func EditBranchProtection(ctx *context.APIContext) {
approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1011,14 +1047,27 @@ func EditBranchProtection(ctx *context.APIContext) {
} else {
approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
}
if form.BypassAllowlistUsernames != nil {
bypassAllowlistUsers, err = user_model.GetUserIDsByNames(ctx, form.BypassAllowlistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
bypassAllowlistUsers = protectBranch.BypassAllowlistUserIDs
}
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
var whitelistTeams, forcePushAllowlistTeams, mergeWhitelistTeams, approvalsWhitelistTeams, bypassAllowlistTeams []int64
if repo.Owner.IsOrganization() {
if form.PushWhitelistTeams != nil {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.PushWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1031,7 +1080,7 @@ func EditBranchProtection(ctx *context.APIContext) {
forcePushAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ForcePushAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1044,7 +1093,7 @@ func EditBranchProtection(ctx *context.APIContext) {
mergeWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.MergeWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1057,7 +1106,7 @@ func EditBranchProtection(ctx *context.APIContext) {
approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.ApprovalsWhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1066,6 +1115,23 @@ func EditBranchProtection(ctx *context.APIContext) {
} else {
approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs
}
if form.BypassAllowlistTeams != nil {
bypassAllowlistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.BypassAllowlistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
return
}
} else {
bypassAllowlistTeams = protectBranch.BypassAllowlistTeamIDs
}
}
if !protectBranch.EnableBypassAllowlist {
bypassAllowlistUsers = nil
bypassAllowlistTeams = nil
}
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
@@ -1077,6 +1143,8 @@ func EditBranchProtection(ctx *context.APIContext) {
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
BypassUserIDs: bypassAllowlistUsers,
BypassTeamIDs: bypassAllowlistTeams,
})
if err != nil {
ctx.APIErrorInternal(err)
@@ -1263,10 +1331,13 @@ func MergeUpstream(ctx *context.APIContext) {
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -9,18 +9,18 @@ import (
"net/http"
"strings"
"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"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
pull_service "gitea.dev/services/pull"
repo_service "gitea.dev/services/repository"
)
// ListCollaborators list a repository's collaborators
@@ -107,7 +107,7 @@ func IsCollaborator(ctx *context.APIContext) {
user, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -167,7 +167,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) {
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -181,12 +181,12 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) {
p := perm.AccessModeWrite
if form.Permission != nil {
p = perm.ParseAccessMode(*form.Permission, perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
p = perm.ParseAccessMode(string(*form.Permission), perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
}
if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -230,7 +230,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
collaborator, err := user_model.GetUserByName(ctx, ctx.PathParam("collaborator"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -284,7 +284,7 @@ func GetRepoPermissions(ctx *context.APIContext) {
collaborator, err := user_model.GetUserByName(ctx, collaboratorUsername)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -326,7 +326,7 @@ func GetReviewers(ctx *context.APIContext) {
canChooseReviewer := issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, ctx.Repo.Repository, 0)
if !canChooseReviewer {
ctx.APIError(http.StatusForbidden, errors.New("doer has no permission to get reviewers"))
ctx.APIError(http.StatusForbidden, "doer has no permission to get reviewers")
return
}
@@ -367,5 +367,42 @@ func GetAssignees(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convert.ToUsers(ctx, ctx.Doer, assignees))
}
// CheckRepoIssueAssignee check if a user can be assigned to issues in a repository
func CheckRepoIssueAssignee(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/assignees/{assignee} repository repoCheckAssignee
// ---
// summary: Check if a user can be assigned to issues in a repository
// 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: assignee
// in: path
// description: username of the user to check for being an assignee
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
if checkAssignableUser(ctx, ctx.PathParam("assignee"), ctx.Repo.Repository) {
ctx.Status(http.StatusNoContent)
}
}
@@ -10,15 +10,15 @@ import (
"strconv"
"time"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetSingleCommit get a commit via sha
@@ -217,7 +217,7 @@ func GetAllCommits(ctx *context.APIContext) {
// get commit specified by sha
baseCommit, err = ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
ctx.NotFoundOrServerError(err)
ctx.APIErrorAuto(err)
return
}
}
@@ -258,11 +258,30 @@ func GetAllCommits(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
} else if commitsCountTotal == 0 {
ctx.APIErrorNotFound("FileCommitsCount", nil)
return
// when date filters are active, a zero count may just mean no
// commits in the requested range — not that the path is invalid
if since == "" && until == "" {
ctx.APIErrorNotFound()
return
}
// verify the path actually exists in the revision history
totalWithoutDate, err := gitrepo.CommitsCount(ctx, ctx.Repo.Repository,
gitrepo.CommitsCountOptions{
Not: not,
Revision: []string{sha},
RelPath: []string{path},
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if totalWithoutDate == 0 {
ctx.APIErrorNotFound()
return
}
}
commits, err = ctx.Repo.GitRepo.CommitsByFileAndRange(
commits, _, err = ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: sha,
File: path,
@@ -383,7 +402,7 @@ func GetCommitPullRequest(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.PathParam("sha"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -6,11 +6,12 @@ package repo
import (
"net/http"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
git_service "gitea.dev/services/git"
)
// CompareDiff compare two branches or commits
@@ -18,8 +19,12 @@ func CompareDiff(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/compare/{basehead} repository repoCompareDiff
// ---
// summary: Get commit comparison information
// description: |
// By default returns JSON commit comparison information. The raw diff or patch can be
// requested with the `output` query parameter set to `diff` or `patch` respectively.
// produces:
// - application/json
// - text/plain
// parameters:
// - name: owner
// in: path
@@ -33,12 +38,21 @@ func CompareDiff(ctx *context.APIContext) {
// required: true
// - name: basehead
// in: path
// description: compare two branches or commits
// description: compare two refs as `base...head` (or `base..head`); refs may be branches, tags, full or short SHAs (including branch names that contain slashes), optionally with a `^` or `~N` revision suffix.
// type: string
// required: true
// - name: output
// in: query
// description: return the raw comparison as `diff` or `patch` instead of JSON
// type: string
// enum:
// - diff
// - patch
// responses:
// "200":
// "$ref": "#/responses/Compare"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
@@ -57,6 +71,16 @@ func CompareDiff(ctx *context.APIContext) {
}
defer closer()
// ?output=diff|patch returns the raw output, otherwise the JSON comparison is returned.
switch ctx.FormString("output") {
case "diff":
downloadCompareDiffOrPatch(ctx, compareInfo, false)
return
case "patch":
downloadCompareDiffOrPatch(ctx, compareInfo, true)
return
}
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
files := ctx.FormString("files") == "" || ctx.FormBool("files")
@@ -88,3 +112,20 @@ func CompareDiff(ctx *context.APIContext) {
Commits: apiCommits,
})
}
// downloadCompareDiffOrPatch writes a comparison's raw diff or patch to the response.
func downloadCompareDiffOrPatch(ctx *context.APIContext, compareInfo *git_service.CompareInfo, patch bool) {
ctx.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
compareArg := compareInfo.BaseCommitID + compareInfo.CompareSeparator + compareInfo.HeadCommitID
var err error
if patch {
err = compareInfo.HeadGitRepo.GetPatch(compareArg, ctx.Resp)
} else {
err = compareInfo.HeadGitRepo.GetDiff(compareArg, ctx.Resp)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
@@ -7,19 +7,19 @@ import (
"errors"
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
archiver_service "code.gitea.io/gitea/services/repository/archiver"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/util"
"gitea.dev/services/context"
archiver_service "gitea.dev/services/repository/archiver"
)
func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []string) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, reqFileName, paths)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -28,7 +28,7 @@ func serveRepoArchive(ctx *context.APIContext, reqFileName string, paths []strin
err = archiver_service.ServeRepoArchive(ctx.Base, aReq)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -14,22 +14,22 @@ import (
"strings"
"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/httplib"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/lfs"
"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/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
pull_service "code.gitea.io/gitea/services/pull"
files_service "code.gitea.io/gitea/services/repository/files"
git_model "gitea.dev/models/git"
"gitea.dev/modules/git"
"gitea.dev/modules/httpcache"
"gitea.dev/modules/httplib"
"gitea.dev/modules/json"
"gitea.dev/modules/lfs"
"gitea.dev/modules/setting"
"gitea.dev/modules/storage"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
"gitea.dev/services/context"
pull_service "gitea.dev/services/pull"
files_service "gitea.dev/services/repository/files"
)
const giteaObjectTypeHeader = "X-Gitea-Object-Type"
@@ -213,7 +213,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn
}
if entry.IsDir() || entry.IsSubModule() {
ctx.APIErrorNotFound("getBlobForEntry", nil)
ctx.APIErrorNotFound()
return nil, nil, nil
}
@@ -301,18 +301,14 @@ func GetEditorconfig(ctx *context.APIContext) {
ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
fileName := ctx.PathParam("filename")
def, err := ec.GetDefinitionForFilename(fileName)
if def == nil {
ctx.APIErrorNotFound(err)
if err != nil {
ctx.APIErrorNotFound(err.Error())
return
}
ctx.JSON(http.StatusOK, def)
@@ -409,7 +405,7 @@ func ChangeFiles(ctx *context.APIContext) {
for _, file := range apiOpts.Files {
contentReader, err := base64Reader(file.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
// FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options
@@ -483,7 +479,7 @@ func CreateFile(ctx *context.APIContext) {
}
contentReader, err := base64Reader(apiOpts.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -554,7 +550,7 @@ func UpdateFile(ctx *context.APIContext) {
}
contentReader, err := base64Reader(apiOpts.ContentBase64)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
willCreate := apiOpts.SHA == ""
@@ -584,17 +580,17 @@ func handleChangeRepoFilesError(ctx *context.APIContext, err error) {
return
}
if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
return
}
if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) ||
files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) ||
files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
if errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -699,10 +695,8 @@ func DeleteFile(ctx *context.APIContext) {
func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int) *utils.RefCommit {
ref = util.IfZero(ref, ctx.Repo.Repository.DefaultBranch)
refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ref, minCommitIDLen...)
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound(err)
} else if err != nil {
ctx.APIErrorInternal(err)
if err != nil {
ctx.APIErrorAuto(err)
}
return refCommit
}
@@ -828,11 +822,8 @@ func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrLi
}
ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound("GetContentsOrList", err)
return nil
}
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return nil
}
return &ret
}
@@ -8,19 +8,19 @@ import (
"errors"
"net/http"
"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/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
repo_service "gitea.dev/services/repository"
)
// ListForks list a repository's forks
@@ -55,7 +55,8 @@ func ListForks(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
listOptions := utils.GetListOptions(ctx)
forks, total, err := repo_service.FindForks(ctx, ctx.Repo.Repository, ctx.Doer, listOptions)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -79,6 +80,7 @@ func ListForks(ctx *context.APIContext) {
apiForks[i] = convert.ToRepo(ctx, fork, permission)
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, apiForks)
}
@@ -165,9 +167,9 @@ func CreateFork(ctx *context.APIContext) {
})
if err != nil {
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -7,11 +7,11 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"gitea.dev/modules/git"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListGitHooks list all Git hooks of a repository
@@ -8,10 +8,10 @@ import (
"net/http"
"net/url"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
)
// GetGitAllRefs get ref or an list all the refs of a repository
@@ -7,19 +7,19 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
webhook_service "code.gitea.io/gitea/services/webhook"
"gitea.dev/models/db"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
"gitea.dev/models/webhook"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
webhook_module "gitea.dev/modules/webhook"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
webhook_service "gitea.dev/services/webhook"
)
// ListHooks list all hooks of a repository
@@ -177,7 +177,7 @@ func TestHook(ctx *context.APIContext) {
commit := convert.ToPayloadCommit(ctx, ctx.Repo.Repository, ctx.Repo.Commit)
commitID := ctx.Repo.Commit.ID.String()
if err := webhook_service.PrepareWebhook(ctx, hook, webhook_module.HookEventPush, &api.PushPayload{
if err := webhook_service.PrepareTestWebhook(ctx, hook, webhook_module.HookEventPush, &api.PushPayload{
Ref: ref,
Before: commitID,
After: commitID,
@@ -5,11 +5,13 @@ package repo
import (
"net/http"
"strconv"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/services/contexttest"
"gitea.dev/models/db"
"gitea.dev/models/unittest"
"gitea.dev/models/webhook"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -17,8 +19,17 @@ import (
func TestTestHook(t *testing.T) {
unittest.PrepareTestEnv(t)
hook := &webhook.Webhook{
RepoID: 1,
URL: "https://www.example.com/test_hook",
ContentType: webhook.ContentTypeJSON,
Events: `{"push_only":true}`,
IsActive: true,
}
assert.NoError(t, db.Insert(t.Context(), hook))
ctx, _ := contexttest.MockAPIContext(t, "user2/repo1/wiki/_pages")
ctx.SetPathParam("id", "1")
ctx.SetPathParam("id", strconv.FormatInt(hook.ID, 10))
contexttest.LoadRepo(t, ctx, 1)
contexttest.LoadRepoCommit(t, ctx)
contexttest.LoadUser(t, ctx, 2)
@@ -26,6 +37,6 @@ func TestTestHook(t *testing.T) {
assert.Equal(t, http.StatusNoContent, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
HookID: 1,
HookID: hook.ID,
}, unittest.Cond("is_delivered=?", false))
}
+60 -122
View File
@@ -12,82 +12,26 @@ import (
"strings"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
issue_indexer "gitea.dev/modules/indexer/issues"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// buildSearchIssuesRepoIDs builds the list of repository IDs for issue search based on query parameters.
// It returns repoIDs, allPublic flag, and any error that occurred.
func buildSearchIssuesRepoIDs(ctx *context.APIContext) (repoIDs []int64, allPublic bool, err error) {
opts := repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
Collaborate: optional.None[bool](),
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
OrderBy: db.SearchOrderByAlphabetically,
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = true
opts.AllLimited = true
}
opts.ApplyPublicOnly(ctx.PublicOnly)
if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
return nil, false, err
}
opts.OwnerID = owner.ID
opts.AllLimited = false
opts.AllPublic = false
opts.Collaborate = optional.Some(false)
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
return nil, false, util.NewInvalidArgumentErrorf("owner organisation is required for filtering on team")
}
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil {
return nil, false, err
}
opts.TeamID = team.ID
}
if opts.AllPublic {
allPublic = true
opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
}
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
if err != nil {
return nil, false, err
}
if len(repoIDs) == 0 {
// no repos found, don't let the indexer return all repos
repoIDs = []int64{0}
}
return repoIDs, allPublic, nil
}
// SearchIssues searches for issues across the repositories that the user has access to
func SearchIssues(ctx *context.APIContext) {
// swagger:operation GET /repos/issues/search issue issueSearchIssues
@@ -187,16 +131,21 @@ func SearchIssues(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
repoIDs, allPublic, err := buildSearchIssuesRepoIDs(ctx)
repoIDs, allPublic, err := common.SearchIssuesRepoIDs(ctx, common.SearchIssuesRepoIDsOptions{
Doer: ctx.Doer,
PublicOnly: ctx.PublicOnly,
OwnerName: ctx.FormString("owner"),
TeamName: ctx.FormString("team"),
})
if err != nil {
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -204,10 +153,6 @@ func SearchIssues(ctx *context.APIContext) {
}
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
keyword = ""
}
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
var includedAnyLabels []int64
@@ -384,15 +329,12 @@ func ListIssues(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
keyword = ""
}
var labelIDs []int64
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
@@ -435,22 +377,16 @@ func ListIssues(ctx *context.APIContext) {
listOptions := utils.GetListOptions(ctx)
isPull := optional.None[bool]()
switch ctx.FormString("type") {
case "pulls":
isPull = optional.Some(true)
case "issues":
isPull = optional.Some(false)
}
isPull := common.ParseIssueFilterTypeIsPull(ctx.FormString("type"))
if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
if isPull.Has() && !ctx.Repo.Permission.CanReadIssuesOrPulls(isPull.Value()) {
ctx.APIErrorNotFound()
return
}
if !isPull.Has() {
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
canReadIssues := ctx.Repo.Permission.CanRead(unit.TypeIssues)
canReadPulls := ctx.Repo.Permission.CanRead(unit.TypePullRequests)
if !canReadIssues && !canReadPulls {
ctx.APIErrorNotFound()
return
@@ -540,16 +476,10 @@ func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
}
user, err := user_model.GetUserByName(ctx, userName)
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound(err)
return 0
}
if err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return 0
}
return user.ID
}
@@ -592,7 +522,7 @@ func GetIssue(ctx *context.APIContext) {
}
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
@@ -639,7 +569,7 @@ func CreateIssue(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateIssueOption)
var deadlineUnix timeutil.TimeStamp
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
if form.Deadline != nil && ctx.Repo.Permission.CanWrite(unit.TypeIssues) {
deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
}
@@ -656,7 +586,7 @@ func CreateIssue(ctx *context.APIContext) {
assigneeIDs := make([]int64, 0)
var err error
if ctx.Repo.CanWrite(unit.TypeIssues) {
if ctx.Repo.Permission.CanWrite(unit.TypeIssues) {
issue.MilestoneID = form.Milestone
assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
if err != nil {
@@ -676,13 +606,13 @@ func CreateIssue(ctx *context.APIContext) {
return
}
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository, false)
valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !valid {
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name}.Error())
return
}
}
@@ -691,11 +621,11 @@ func CreateIssue(ctx *context.APIContext) {
form.Labels = make([]int64, 0)
}
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.APIError(http.StatusBadRequest, err)
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, form.Projects); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err.Error())
} else if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -776,7 +706,7 @@ func EditIssue(ctx *context.APIContext) {
return
}
issue.Repo = ctx.Repo.Repository
canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
err = issue.LoadAttributes(ctx)
if err != nil {
@@ -794,7 +724,7 @@ func EditIssue(ctx *context.APIContext) {
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error())
return
}
@@ -813,7 +743,7 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
@@ -869,7 +799,7 @@ func EditIssue(ctx *context.APIContext) {
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -914,6 +844,18 @@ func EditIssue(ctx *context.APIContext) {
}
}
// Update projects if provided
if canWrite && form.Projects != nil {
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, *form.Projects); err != nil {
if errors.Is(err, util.ErrPermissionDenied) || errors.Is(err, util.ErrNotExist) {
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
return
}
}
// Refetch from database to assign some automatic values
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
@@ -957,11 +899,7 @@ func DeleteIssue(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -1021,7 +959,7 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "Not repo writer")
return
}
@@ -0,0 +1,238 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// AddIssueAssignees add assignees to an issue
func AddIssueAssignees(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assignees issue issueAddAssignees
// ---
// summary: Add assignees to an issue
// 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: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/IssueAssigneesOption"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
updateIssueAssignees(ctx, *opts, true)
}
// DeleteIssueAssignees remove assignees from an issue
func DeleteIssueAssignees(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assignees issue issueRemoveAssignees
// ---
// summary: Remove assignees from an issue
// 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: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/IssueAssigneesOption"
// responses:
// "200":
// "$ref": "#/responses/Issue"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
updateIssueAssignees(ctx, *opts, false)
}
// CheckIssueAssignee check if a user can be assigned to an issue
func CheckIssueAssignee(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assignees/{assignee} issue issueCheckAssignee
// ---
// summary: Check if a user can be assigned to an issue
// 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: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: assignee
// in: path
// description: username of the user to check for being an assignee
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if checkAssignableUser(ctx, ctx.PathParam("assignee"), ctx.Repo.Repository) {
ctx.Status(http.StatusNoContent)
}
}
// checkAssignableUser resolves assigneeName and verifies the user can be assigned to issues in repo.
// Returns true only when the user resolves AND is assignable; the caller is responsible for writing the 204.
// On any failure it writes the appropriate API response and returns false.
func checkAssignableUser(ctx *context.APIContext, assigneeName string, repo *repo_model.Repository) bool {
assignee, err := user_model.GetUserByName(ctx, assigneeName)
if err != nil {
ctx.APIErrorAuto(err)
return false
}
canAssign, err := access_model.CanBeAssigned(ctx, assignee, repo)
if err != nil {
ctx.APIErrorAuto(err)
return false
}
if !canAssign {
ctx.APIErrorNotFound()
return false
}
return true
}
func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption, isAdd bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
if err := issue.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
assigneeIDs, err := user_model.GetUserIDsByNames(ctx, opts.Assignees, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorAuto(err)
return
}
if isAdd {
err = issue_service.AddAssignees(ctx, issue, ctx.Doer, assigneeIDs)
} else {
err = issue_service.RemoveAssignees(ctx, issue, ctx.Doer, assigneeIDs)
}
if err != nil {
ctx.APIErrorAuto(err)
return
}
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
status := http.StatusOK
if isAdd {
status = http.StatusCreated
}
ctx.JSON(status, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}
@@ -7,18 +7,18 @@ import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"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/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
attachment_service "gitea.dev/services/attachment"
"gitea.dev/services/context"
"gitea.dev/services/context/upload"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// GetIssueAttachment gets a single attachment of the issue
@@ -194,9 +194,9 @@ func CreateIssueAttachment(ctx *context.APIContext) {
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -272,7 +272,7 @@ func EditIssueAttachment(ctx *context.APIContext) {
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -336,7 +336,7 @@ func DeleteIssueAttachment(ctx *context.APIContext) {
func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.NotFoundOrServerError(err)
ctx.APIErrorAuto(err)
return nil
}
@@ -361,7 +361,7 @@ func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment
func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Issue) *repo_model.Attachment {
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id"))
if err != nil {
ctx.NotFoundOrServerError(err)
ctx.APIErrorAuto(err)
return nil
}
if !attachmentBelongsToRepoOrIssue(ctx, attachment, issue) {
@@ -371,7 +371,7 @@ func getIssueAttachmentSafeRead(ctx *context.APIContext, issue *issues_model.Iss
}
func canUserWriteIssueAttachment(ctx *context.APIContext, issue *issues_model.Issue) bool {
canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull))
canEditIssue := ctx.IsSigned && (ctx.Doer.ID == issue.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin() || ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull))
if !canEditIssue {
ctx.APIError(http.StatusForbidden, "user should have permission to write issue")
return false
@@ -9,18 +9,18 @@ import (
"errors"
"net/http"
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/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/optional"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// ListIssueComments list all the comments of an issue
@@ -65,7 +65,7 @@ func ListIssueComments(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
@@ -73,7 +73,7 @@ func ListIssueComments(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
@@ -169,7 +169,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
@@ -274,13 +274,13 @@ func ListRepoIssueComments(ctx *context.APIContext) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
var isPull optional.Option[bool]
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
canReadIssue := ctx.Repo.Permission.CanRead(unit.TypeIssues)
canReadPull := ctx.Repo.Permission.CanRead(unit.TypePullRequests)
if canReadIssue && canReadPull {
isPull = optional.None[bool]()
} else if canReadIssue {
@@ -386,20 +386,20 @@ func CreateIssueComment(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, ctx.Locale.TrString("repo.issues.comment_on_locked"))
return
}
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -447,15 +447,11 @@ func GetIssueComment(ctx *context.APIContext) {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
}
@@ -572,15 +568,11 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
}
@@ -595,7 +587,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
comment.Content = form.Body
if err := issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -681,15 +673,11 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
func deleteIssueComment(ctx *context.APIContext) {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden)
return
} else if !comment.Type.HasContentSupport() {
@@ -7,19 +7,19 @@ import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
attachment_service "gitea.dev/services/attachment"
"gitea.dev/services/context"
"gitea.dev/services/context/upload"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// GetIssueCommentAttachment gets a single attachment of the comment
@@ -202,9 +202,9 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -218,7 +218,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
if err = issue_service.UpdateComment(ctx, comment, comment.ContentVersion, ctx.Doer, comment.Content); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -285,7 +285,7 @@ func EditIssueCommentAttachment(ctx *context.APIContext) {
if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -346,7 +346,7 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) {
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.NotFoundOrServerError(err)
ctx.APIErrorAuto(err)
return nil
}
if err := comment.LoadIssue(ctx); err != nil {
@@ -358,7 +358,7 @@ func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
return nil
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
return nil
}
@@ -379,7 +379,7 @@ func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Att
}
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull)
if !canEditComment {
ctx.APIError(http.StatusForbidden, "user should have permission to edit comment")
return false
@@ -391,7 +391,7 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues
func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment {
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.PathParamInt64("attachment_id"))
if err != nil {
ctx.NotFoundOrServerError(err)
ctx.APIErrorAuto(err)
return nil
}
if !attachmentBelongsToRepoOrComment(ctx, attachment, comment) {
@@ -7,15 +7,15 @@ package repo
import (
"net/http"
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/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetIssueDependencies list an issue's dependencies
@@ -63,11 +63,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -487,11 +483,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil
}
issue.Repo = ctx.Repo.Repository
@@ -508,11 +500,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is
var err error
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorNotFound("IsErrRepoNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil
}
} else {
@@ -521,11 +509,7 @@ func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Is
issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index)
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound("IsErrIssueNotExist", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil
}
issue.Repo = repo
@@ -9,12 +9,12 @@ import (
"net/http"
"reflect"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// ListIssueLabels list all the labels of an issue
@@ -173,18 +173,17 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
label, err := issues_model.GetLabelByID(ctx, ctx.PathParamInt64("id"))
// the label must belong to this repo (or its owning org); otherwise a foreign label ID
// is rejected the same way as a nonexistent one, closing a cross-repo enumeration oracle
labelID := ctx.PathParamInt64("id")
label, err := issues_model.GetLabelInRepoOrOrgByID(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner.ID, ctx.Repo.Owner.IsOrganization(), labelID)
if err != nil {
if issues_model.IsErrLabelNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -295,7 +294,7 @@ func ClearIssueLabels(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
@@ -319,7 +318,7 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
return nil, nil, err
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "write permission is required")
return nil, nil, errors.New("permission denied")
}
@@ -4,13 +4,12 @@
package repo
import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/context"
)
// LockIssue lock an issue
@@ -54,16 +53,12 @@ func LockIssue(ctx *context.APIContext) {
reason := web.GetForm(ctx).(*api.LockIssueOption).Reason
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to lock this issue"))
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to lock this issue")
return
}
@@ -121,16 +116,12 @@ func UnlockIssue(ctx *context.APIContext) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to unlock this issue"))
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to unlock this issue")
return
}
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// PinIssue pins a issue
@@ -46,7 +46,7 @@ func PinIssue(ctx *context.APIContext) {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound()
} else if issues_model.IsErrIssueMaxPinReached(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -7,14 +7,14 @@ import (
"errors"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
)
// GetIssueCommentReactions list reactions of a comment from an issue
@@ -53,11 +53,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -71,8 +67,8 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to get reactions")
return
}
@@ -190,11 +186,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
comment, err := issues_model.GetCommentByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -208,13 +200,13 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
if comment.Issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
return
}
@@ -223,7 +215,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
@@ -304,8 +296,8 @@ func GetIssueReactions(ctx *context.APIContext) {
return
}
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to get reactions"))
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to get reactions")
return
}
@@ -428,8 +420,8 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
return
}
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, errors.New("no permission to change reaction"))
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.APIError(http.StatusForbidden, "no permission to change reaction")
return
}
@@ -438,7 +430,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
if err != nil {
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else if issues_model.IsErrReactionAlreadyExist(err) {
ctx.JSON(http.StatusOK, api.Reaction{
User: convert.ToUser(ctx, ctx.Doer, ctx.Doer),
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// StartIssueStopwatch creates a stopwatch for the given issue.
@@ -178,7 +178,7 @@ func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
return nil
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return nil
}
@@ -7,12 +7,12 @@ import (
"fmt"
"net/http"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// AddIssueSubscription Subscribe user to issue
@@ -128,7 +128,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
// only admin and user for itself can change subscription
if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.APIError(http.StatusForbidden, fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name))
ctx.APIError(http.StatusForbidden, fmt.Sprintf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name))
return
}
@@ -4,19 +4,19 @@
package repo
import (
"errors"
"net/http"
"time"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListTrackedTimes list all the tracked times of an issue
@@ -72,16 +72,12 @@ func ListTrackedTimes(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"
if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
ctx.APIErrorNotFound("Timetracker is disabled")
ctx.APIErrorNotFound("timetracker is disabled")
return
}
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -95,7 +91,8 @@ func ListTrackedTimes(ctx *context.APIContext) {
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
@@ -104,7 +101,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
}
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -116,7 +113,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
if opts.UserID == 0 {
opts.UserID = ctx.Doer.ID
} else {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights")
return
}
}
@@ -136,11 +133,33 @@ func ListTrackedTimes(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
}
func filterTrackedTimesByAccess(ctx *context.APIContext, trackedTimes issues_model.TrackedTimeList) (issues_model.TrackedTimeList, error) {
filtered := make(issues_model.TrackedTimeList, 0, len(trackedTimes))
for _, trackedTime := range trackedTimes {
if trackedTime.Issue == nil || trackedTime.Issue.Repo == nil {
continue
}
permission, err := access_model.GetIndividualUserRepoPermission(ctx, trackedTime.Issue.Repo, ctx.Doer)
if err != nil {
return nil, err
}
if permission.HasAnyUnitAccessOrPublicAccess() {
filtered = append(filtered, trackedTime)
}
}
return filtered, nil
}
// AddTime add time manual to the given issue
func AddTime(ctx *context.APIContext) {
// swagger:operation Post /repos/{owner}/{repo}/issues/{index}/times issue issueAddTime
@@ -183,11 +202,7 @@ func AddTime(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.AddTimeOption)
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -206,7 +221,8 @@ func AddTime(ctx *context.APIContext) {
// allow only RepoAdmin, Admin and User to add time
user, err = user_model.GetUserByName(ctx, form.User)
if err != nil {
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
}
}
@@ -266,11 +282,7 @@ func ResetIssueTime(ctx *context.APIContext) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -286,7 +298,7 @@ func ResetIssueTime(ctx *context.APIContext) {
err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -339,11 +351,7 @@ func DeleteTime(ctx *context.APIContext) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -358,11 +366,7 @@ func DeleteTime(ctx *context.APIContext) {
time, err := issues_model.GetTrackedTimeByID(ctx, issue.ID, ctx.PathParamInt64("id"))
if err != nil {
if db.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
return
}
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
if time.Deleted {
@@ -424,11 +428,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
}
user, err := user_model.GetUserByName(ctx, ctx.PathParam("timetrackingusername"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
if user == nil {
@@ -437,7 +437,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
}
if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights")
return
}
@@ -523,7 +523,8 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
if qUser != "" {
user, err := user_model.GetUserByName(ctx, qUser)
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
} else if err != nil {
ctx.APIErrorInternal(err)
return
@@ -533,7 +534,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -545,7 +546,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
if opts.UserID == 0 {
opts.UserID = ctx.Doer.ID
} else {
ctx.APIError(http.StatusForbidden, errors.New("query by user not allowed; not enough rights"))
ctx.APIError(http.StatusForbidden, "query by user not allowed; not enough rights")
return
}
}
@@ -565,6 +566,11 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
@@ -607,7 +613,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -627,6 +633,11 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
}
trackedTimes, err = filterTrackedTimesByAccess(ctx, trackedTimes)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
+13 -13
View File
@@ -10,18 +10,18 @@ import (
"net/http"
"net/url"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"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"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
asymkey_model "gitea.dev/models/asymkey"
"gitea.dev/models/db"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// appendPrivateInformation appends the owner and key type information to api.PublicKey
@@ -179,7 +179,7 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
} else if asymkey_model.IsErrKeyUnableVerify(err) {
ctx.APIError(http.StatusUnprocessableEntity, "Unable to verify key content")
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid key content: %w", err))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid key content: %v", err))
}
}
@@ -8,13 +8,13 @@ import (
"net/http"
"strconv"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/label"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issues_model "gitea.dev/models/issues"
"gitea.dev/modules/label"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListLabels list all the labels of a repository
@@ -106,11 +106,7 @@ func GetLabel(ctx *context.APIContext) {
l, err = issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, intID)
}
if err != nil {
if issues_model.IsErrRepoLabelNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -153,7 +149,7 @@ func CreateLabel(ctx *context.APIContext) {
color, err := label.NormalizeColor(form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
form.Color = color
@@ -214,11 +210,7 @@ func EditLabel(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.EditLabelOption)
l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrRepoLabelNotExist(err) {
ctx.APIErrorNotFound()
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -231,7 +223,7 @@ func EditLabel(ctx *context.APIContext) {
if form.Color != nil {
color, err := label.NormalizeColor(*form.Color)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
l.Color = color
@@ -8,9 +8,9 @@ import (
"net/http"
"strconv"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/services/context"
)
type languageResponse []*repo_model.LanguageStat
@@ -6,9 +6,9 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/services/context"
)
// GetLicenses returns licenses
@@ -6,9 +6,9 @@ package repo
import (
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
webhook_service "code.gitea.io/gitea/services/webhook"
"gitea.dev/models/unittest"
"gitea.dev/modules/setting"
webhook_service "gitea.dev/services/webhook"
)
func TestMain(m *testing.M) {
@@ -10,26 +10,26 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
"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/git"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"
notify_service "code.gitea.io/gitea/services/notify"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/graceful"
"gitea.dev/modules/lfs"
"gitea.dev/modules/log"
base "gitea.dev/modules/migration"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/migrations"
notify_service "gitea.dev/services/notify"
repo_service "gitea.dev/services/repository"
)
// Migrate migrate remote git repository to gitea
@@ -72,18 +72,13 @@ func Migrate(ctx *context.APIContext) {
}
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
return
}
if ctx.HasAPIError() {
ctx.APIError(http.StatusUnprocessableEntity, ctx.GetErrMsg())
return
}
if !ctx.Doer.IsAdmin {
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
ctx.APIError(http.StatusForbidden, "Given user is not an organization.")
@@ -115,12 +110,12 @@ func Migrate(ctx *context.APIContext) {
gitServiceType := convert.ToGitServiceType(form.Service)
if form.Mirror && setting.Mirror.DisableNewPull {
ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled the creation of new pull mirrors"))
ctx.APIError(http.StatusForbidden, "the site administrator has disabled the creation of new pull mirrors")
return
}
if setting.Repository.DisableMigrations {
ctx.APIError(http.StatusForbidden, errors.New("the site administrator has disabled migrations"))
ctx.APIError(http.StatusForbidden, "the site administrator has disabled migrations")
return
}
@@ -240,9 +235,9 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *user_model.User, err
case db.IsErrNamePatternNotAllowed(err):
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(db.ErrNamePatternNotAllowed).Pattern))
case git.IsErrInvalidCloneAddr(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case base.IsErrNotSupported(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
default:
err = util.SanitizeErrorCredentialURLs(err)
if strings.Contains(err.Error(), "Authentication failed") ||
@@ -262,7 +257,7 @@ func handleRemoteAddrError(ctx *context.APIContext, err error) {
addrErr := err.(*git.ErrInvalidCloneAddr)
switch {
case addrErr.IsURLError:
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, "The provided URL is invalid.")
case addrErr.IsPermissionDenied:
if addrErr.LocalPath {
ctx.APIError(http.StatusUnprocessableEntity, "You are not allowed to import local repositories.")
@@ -8,15 +8,15 @@ import (
"net/http"
"strconv"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListMilestones list milestones for a repository
@@ -5,22 +5,24 @@ package repo
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"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/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"
mirror_service "code.gitea.io/gitea/services/mirror"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/migrations"
mirror_service "gitea.dev/services/mirror"
)
// MirrorSync adds a mirrored repository to the sync queue
@@ -51,7 +53,7 @@ func MirrorSync(ctx *context.APIContext) {
repo := ctx.Repo.Repository
if !ctx.Repo.CanWrite(unit.TypeCode) {
if !ctx.Repo.Permission.CanWrite(unit.TypeCode) {
ctx.APIError(http.StatusForbidden, "Must have write access")
}
@@ -101,6 +103,8 @@ func PushMirrorSync(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
@@ -109,17 +113,21 @@ func PushMirrorSync(ctx *context.APIContext) {
// Get All push mirrors of a specific repo
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
if err != nil {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
failedPushMirrors := make([]string, 0)
for _, mirror := range pushMirrors {
ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
if !ok {
ctx.APIErrorInternal(errors.New("error occurred when syncing push mirror " + mirror.RemoteName))
return
failedPushMirrors = append(failedPushMirrors, mirror.RemoteName)
}
}
if len(failedPushMirrors) != 0 {
ctx.APIError(http.StatusUnprocessableEntity, "error occurred when syncing push mirrors: "+strings.Join(failedPushMirrors, ", "))
return
}
ctx.Status(http.StatusOK)
}
@@ -168,7 +176,7 @@ func ListPushMirrors(ctx *context.APIContext) {
// Get all push mirrors for the specified repository.
pushMirrors, count, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, utils.GetListOptions(ctx))
if err != nil {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
@@ -232,7 +240,7 @@ func GetPushMirrorByName(ctx *context.APIContext) {
ctx.APIErrorInternal(err)
return
} else if !exist {
ctx.APIError(http.StatusNotFound, nil)
ctx.APIErrorNotFound()
return
}
@@ -327,7 +335,7 @@ func DeletePushMirrorByRemoteName(ctx *context.APIContext) {
// Delete push mirror on repo by name.
err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{RepoID: ctx.Repo.Repository.ID, RemoteName: remoteName})
if err != nil {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return
}
ctx.Status(http.StatusNoContent)
@@ -337,8 +345,12 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
repo := ctx.Repo.Repository
interval, err := time.ParseDuration(mirrorOption.Interval)
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
ctx.APIError(http.StatusBadRequest, err)
if err != nil {
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid interval: %v", err))
return
}
if interval != 0 && interval < setting.Mirror.MinInterval {
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("interval is shorter than minimum %v", setting.Mirror.MinInterval.String()))
return
}
@@ -351,11 +363,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
return
}
remoteSuffix, err := util.CryptoRandomString(10)
if err != nil {
ctx.APIErrorInternal(err)
return
}
remoteSuffix := util.CryptoRandomString(10)
remoteAddress, err := util.SanitizeURL(mirrorOption.RemoteAddress)
if err != nil {
@@ -0,0 +1,40 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"testing"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
// TestPushMirrorSync verifies the endpoint attempts every push mirror instead
// of aborting on the first failure, reporting all failed remotes with a 422.
// Each remote name is not a configured git remote, so SyncPushMirror fails fast
// without any network access.
func TestPushMirrorSync(t *testing.T) {
unittest.PrepareTestEnv(t)
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
for _, remoteName := range []string{"broken_remote_1", "broken_remote_2"} {
assert.NoError(t, db.Insert(t.Context(), &repo_model.PushMirror{RepoID: 1, RemoteName: remoteName}))
}
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
contexttest.LoadRepo(t, ctx, 1)
PushMirrorSync(ctx)
assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
assert.Contains(t, resp.Body.String(), "broken_remote_1")
assert.Contains(t, resp.Body.String(), "broken_remote_2")
}
@@ -7,10 +7,10 @@ import (
"errors"
"net/http"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"gitea.dev/modules/git"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// GetNote Get a note corresponding to a single commit from a repository
@@ -68,11 +68,7 @@ func getNote(ctx *context.APIContext, identifier string) {
commitID, err := ctx.Repo.GitRepo.ConvertToGitID(identifier)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -6,10 +6,10 @@ package repo
import (
"net/http"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/repository/files"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/services/context"
"gitea.dev/services/repository/files"
)
// ApplyDiffPatch handles API call for applying a patch
@@ -12,39 +12,38 @@ import (
"strings"
"time"
activities_model "code.gitea.io/gitea/models/activities"
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"
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/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"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/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
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/convert"
"code.gitea.io/gitea/services/forms"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
activities_model "gitea.dev/models/activities"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
access_model "gitea.dev/models/perm/access"
pull_model "gitea.dev/models/pull"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/routers/common"
asymkey_service "gitea.dev/services/asymkey"
"gitea.dev/services/automerge"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"gitea.dev/services/forms"
git_service "gitea.dev/services/git"
"gitea.dev/services/gitdiff"
issue_service "gitea.dev/services/issue"
notify_service "gitea.dev/services/notify"
pull_service "gitea.dev/services/pull"
repo_service "gitea.dev/services/repository"
)
// ListPullRequests returns a list of all PRs
@@ -126,7 +125,7 @@ func ListPullRequests(ctx *context.APIContext) {
poster, err := user_model.GetUserByName(ctx, posterStr)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -448,7 +447,7 @@ func CreatePullRequest(ctx *context.APIContext) {
HeadBranch: existingPr.HeadBranch,
BaseBranch: existingPr.BaseBranch,
}
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
@@ -521,7 +520,7 @@ func CreatePullRequest(ctx *context.APIContext) {
BaseBranch: compareResult.BaseRef.ShortName(),
HeadRepo: compareResult.HeadRepo,
BaseRepo: repo,
MergeBase: compareResult.MergeBase,
MergeBase: compareResult.CompareBase,
Type: issues_model.PullRequestGitea,
}
@@ -545,13 +544,13 @@ func CreatePullRequest(ctx *context.APIContext) {
return
}
valid, err := access_model.CanBeAssigned(ctx, assignee, repo, true)
valid, err := access_model.CanBeAssigned(ctx, assignee, repo)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !valid {
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name})
ctx.APIError(http.StatusUnprocessableEntity, repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: repo.Name}.Error())
return
}
}
@@ -570,11 +569,11 @@ func CreatePullRequest(ctx *context.APIContext) {
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else if errors.Is(err, issues_model.ErrMustCollaborator) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -653,7 +652,7 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypePullRequests) {
if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWrite(unit.TypePullRequests) {
ctx.Status(http.StatusForbidden)
return
}
@@ -663,7 +662,7 @@ func EditPullRequest(ctx *context.APIContext) {
// handles concurrent requests.
// TODO: wrap all mutations in a transaction to fully prevent partial writes.
if form.ContentVersion != nil && *form.ContentVersion != issue.ContentVersion {
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged)
ctx.APIError(http.StatusConflict, issues_model.ErrIssueAlreadyChanged.Error())
return
}
@@ -682,7 +681,7 @@ func EditPullRequest(ctx *context.APIContext) {
err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body, contentVersion)
if err != nil {
if errors.Is(err, issues_model.ErrIssueAlreadyChanged) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
@@ -715,13 +714,13 @@ func EditPullRequest(ctx *context.APIContext) {
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
if ctx.Repo.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && (form.Assignees != nil || len(form.Assignee) > 0) {
err = issue_service.UpdateAssignees(ctx, issue, form.Assignee, form.Assignees, ctx.Doer)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("Assignee does not exist: [name: %s]", err))
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -729,7 +728,7 @@ func EditPullRequest(ctx *context.APIContext) {
}
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Milestone != 0 &&
issue.MilestoneID != form.Milestone {
oldMilestoneID := issue.MilestoneID
issue.MilestoneID = form.Milestone
@@ -744,7 +743,7 @@ func EditPullRequest(ctx *context.APIContext) {
}
}
if ctx.Repo.CanWrite(unit.TypePullRequests) && form.Labels != nil {
if ctx.Repo.Permission.CanWrite(unit.TypePullRequests) && form.Labels != nil {
labels, err := issues_model.GetLabelsInRepoByIDs(ctx, ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.APIErrorInternal(err)
@@ -788,18 +787,18 @@ func EditPullRequest(ctx *context.APIContext) {
return
}
if !branchExist {
ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base))
ctx.APIError(http.StatusNotFound, fmt.Sprintf("new base '%s' not exist", form.Base))
return
}
if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, form.Base); err != nil {
if issues_model.IsErrPullRequestAlreadyExists(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
} else if issues_model.IsErrIssueIsClosed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
} else if pull_service.IsErrPullRequestHasMerged(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -927,11 +926,7 @@ func MergePullRequest(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -965,7 +960,7 @@ func MergePullRequest(ctx *context.APIContext) {
}
// start with merging by checking
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
if err := pull_service.CheckPullMergeable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, repo_model.MergeStyle(form.Do), form.ForceMerge); err != nil {
if errors.Is(err, pull_service.ErrIsClosed) {
ctx.APIErrorNotFound()
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
@@ -977,9 +972,11 @@ func MergePullRequest(ctx *context.APIContext) {
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
ctx.APIError(http.StatusMethodNotAllowed, "Please try again later")
} else if errors.Is(err, pull_service.ErrNotReadyToMerge) {
ctx.APIError(http.StatusMethodNotAllowed, err)
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
} else if asymkey_service.IsErrWontSign(err) {
ctx.APIError(http.StatusMethodNotAllowed, err)
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
} else if errors.Is(err, pull_service.ErrHeadCommitsNotAllVerified) {
ctx.APIError(http.StatusMethodNotAllowed, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -990,11 +987,11 @@ func MergePullRequest(ctx *context.APIContext) {
if manuallyMerged {
if err := pull_service.MergedManually(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
return
}
if strings.Contains(err.Error(), "Wrong commit ID") {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1032,7 +1029,7 @@ func MergePullRequest(ctx *context.APIContext) {
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
if err != nil {
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1044,9 +1041,9 @@ func MergePullRequest(ctx *context.APIContext) {
}
}
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if err := pull_service.Merge(pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
if pull_service.IsErrInvalidMergeStyle(err) {
ctx.APIError(http.StatusMethodNotAllowed, fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
ctx.APIError(http.StatusMethodNotAllowed, fmt.Sprintf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do)))
} else if pull_service.IsErrMergeConflicts(err) {
conflictError := err.(pull_service.ErrMergeConflicts)
ctx.JSON(http.StatusConflict, conflictError)
@@ -1089,12 +1086,6 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
baseRepo := ctx.Repo.Repository
compareReq := common.ParseCompareRouterParam(compareParam)
// remove the check when we support compare with carets
if compareReq.BaseOriRefSuffix != "" {
ctx.APIError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix")
return nil, nil
}
_, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
switch {
case errors.Is(err, util.ErrInvalidArgument):
@@ -1107,6 +1098,10 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
ctx.APIErrorInternal(err)
return nil, nil
}
if !ctx.TokenCanAccessRepo(headRepo) {
ctx.APIErrorNotFound()
return nil, nil
}
isSameRepo := baseRepo.ID == headRepo.ID
@@ -1154,10 +1149,18 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
return nil, nil
}
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)))
headRef := headGitRepo.UnstableGuessRefByShortName(util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch))
baseRef, err := common.ResolveRefWithSuffix(ctx.Repo.GitRepo, util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx)), compareReq.BaseOriRefSuffix)
if err != nil {
ctx.APIErrorAuto(err)
return nil, nil
}
headRef, err := common.ResolveRefWithSuffix(headGitRepo, util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch), compareReq.HeadOriRefSuffix)
if err != nil {
ctx.APIErrorAuto(err)
return nil, nil
}
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef, baseRef, compareReq.HeadOriRef, headRef)
log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), compareReq.BaseOriRef+compareReq.BaseOriRefSuffix, baseRef, compareReq.HeadOriRef+compareReq.HeadOriRefSuffix, headRef)
baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName())
headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName())
@@ -1173,7 +1176,7 @@ func parseCompareInfo(ctx *context.APIContext, compareParam string) (result *git
return nil, nil
}
return compareInfo, closer
return &compareInfo, closer
}
// UpdatePullRequest merge PR's baseBranch into headBranch
@@ -1228,7 +1231,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
}
if pr.HasMerged {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, "pull request is already merged")
return
}
@@ -1238,7 +1241,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
}
if pr.Issue.IsClosed {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, "pull request is already closed")
return
}
@@ -1251,15 +1254,24 @@ func UpdatePullRequest(ctx *context.APIContext) {
return
}
rebase := ctx.FormString("style") == "rebase"
// a public-only token must not update (push into) a private head repo,
// even when the base repo named in the route is public
if !ctx.TokenCanAccessRepo(pr.HeadRepo) {
ctx.APIErrorNotFound()
return
}
allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, pr, ctx.Doer)
// keep API back-compat: when no style is given, default to "merge" rather than the repo's DefaultUpdateStyle,
// so existing API clients keep getting a merge update.
rebase := repo_model.UpdateStyle(ctx.FormString("style", string(repo_model.UpdateStyleMerge))) == repo_model.UpdateStyleRebase
userUpdateStyles, err := pull_service.CheckUserAllowedToUpdate(ctx, pr, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
if (rebase && !userUpdateStyles.RebaseAllowed) || (!rebase && !userUpdateStyles.MergeAllowed) {
ctx.Status(http.StatusForbidden)
return
}
@@ -1267,7 +1279,7 @@ func UpdatePullRequest(ctx *context.APIContext) {
// default merge commit message
message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch)
if err = pull_service.Update(graceful.GetManager().ShutdownContext(), pr, ctx.Doer, message, rebase); err != nil {
if err = pull_service.Update(pr, ctx.Doer, message, rebase); err != nil {
if pull_service.IsErrMergeConflicts(err) {
ctx.APIError(http.StatusConflict, "merge failed because of conflict")
return
@@ -1417,7 +1429,6 @@ func GetPullRequestCommits(ctx *context.APIContext) {
return
}
var compareInfo *git_service.CompareInfo
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
ctx.APIErrorInternal(err)
@@ -1425,13 +1436,14 @@ func GetPullRequestCommits(ctx *context.APIContext) {
}
defer closer.Close()
var compareInfo git_service.CompareInfo
if pr.HasMerged {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefNameFromBranch(pr.BaseBranch), git.RefName(pr.GetGitHeadRefName()), false, false)
}
if gitcmd.StderrHasPrefix(err, "fatal: bad revision") {
if gitcmd.IsStderr(err, gitcmd.StderrBadRevision) {
ctx.APIError(http.StatusNotFound, "invalid base branch or revision")
return
} else if err != nil {
@@ -1550,7 +1562,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
baseGitRepo := ctx.Repo.GitRepo
var compareInfo *git_service.CompareInfo
var compareInfo git_service.CompareInfo
if pr.HasMerged {
compareInfo, err = git_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, git.RefName(pr.MergeBase), git.RefName(pr.GetGitHeadRefName()), false, false)
} else {
@@ -1567,7 +1579,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
return
}
startCommitID := compareInfo.MergeBase
startCommitID := compareInfo.CompareBase
endCommitID := headCommitID
maxLines := setting.Git.MaxGitDiffLines
@@ -9,18 +9,18 @@ import (
"net/http"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/organization"
access_model "gitea.dev/models/perm/access"
user_model "gitea.dev/models/user"
"gitea.dev/modules/gitrepo"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
issue_service "gitea.dev/services/issue"
pull_service "gitea.dev/services/pull"
)
// ListPullReviews lists all reviews of a pull request
@@ -63,11 +63,7 @@ func ListPullReviews(ctx *context.APIContext) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -208,6 +204,88 @@ func GetPullReviewComments(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiComments)
}
// CreatePullReviewCommentReply replies to a pull request review comment.
// The URL mirrors GitHub's endpoint, {index} is verified against the parent comment's pull request.
func CreatePullReviewCommentReply(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/comments/{id}/replies repository repoCreatePullReviewCommentReply
// ---
// summary: Reply to a pull request review comment
// 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: index
// in: path
// description: index of the pull request
// type: integer
// format: int64
// required: true
// - name: id
// in: path
// description: id of the review comment to reply to
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreatePullReviewCommentReplyOptions"
// responses:
// "201":
// "$ref": "#/responses/PullReviewComment"
// "400":
// "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.CreatePullReviewCommentReplyOptions)
parent := getPullReviewCommentToResolve(ctx)
if parent == nil {
return
}
if parent.Issue.Index != ctx.PathParamInt64("index") {
ctx.APIErrorNotFound()
return
}
if parent.ReviewID == 0 {
ctx.APIError(http.StatusBadRequest, "comment is not a review comment")
return
}
comment, err := pull_service.CreateCodeComment(ctx,
ctx.Doer, ctx.Repo.GitRepo, parent.Issue,
parent.Line, opts.Body, parent.TreePath,
false, parent.ReviewID,
"", nil,
)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if err := comment.LoadPoster(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
comment.Issue = parent.Issue
ctx.JSON(http.StatusCreated, convert.ToPullReviewComment(ctx, comment, ctx.Doer))
}
// ResolvePullReviewComment resolves a review comment in a pull request
func ResolvePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/comments/{id}/resolve repository repoResolvePullReviewComment
@@ -307,11 +385,7 @@ func updatePullReviewCommentResolve(ctx *context.APIContext, isResolve bool) {
func getPullReviewCommentToResolve(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentWithRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.APIErrorNotFound("GetCommentByID", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil
}
@@ -376,7 +450,7 @@ func DeletePullReview(ctx *context.APIContext) {
return
}
if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID {
ctx.APIError(http.StatusForbidden, nil)
ctx.APIError(http.StatusForbidden, "no permission to delete comment")
return
}
@@ -392,7 +466,7 @@ func DeletePullReview(ctx *context.APIContext) {
func CreatePullReview(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews repository repoCreatePullReview
// ---
// summary: Create a review to an pull request
// summary: Create a review to a pull request
// produces:
// - application/json
// parameters:
@@ -428,11 +502,7 @@ func CreatePullReview(ctx *context.APIContext) {
opts := web.GetForm(ctx).(*api.CreatePullReviewOptions)
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -493,7 +563,7 @@ func CreatePullReview(ctx *context.APIContext) {
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
if err != nil {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -509,11 +579,11 @@ func CreatePullReview(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiReview)
}
// SubmitPullReview submit a pending review to an pull request
// SubmitPullReview submit a pending review to a pull request
func SubmitPullReview(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoSubmitPullReview
// ---
// summary: Submit a pending review to an pull request
// summary: Submit a pending review to a pull request
// produces:
// - application/json
// parameters:
@@ -559,7 +629,7 @@ func SubmitPullReview(ctx *context.APIContext) {
}
if review.Type != issues_model.ReviewTypePending {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("only a pending review can be submitted"))
ctx.APIError(http.StatusUnprocessableEntity, "only a pending review can be submitted")
return
}
@@ -571,7 +641,7 @@ func SubmitPullReview(ctx *context.APIContext) {
// if review stay pending return
if reviewType == issues_model.ReviewTypePending {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("review stay pending"))
ctx.APIError(http.StatusUnprocessableEntity, "review stay pending")
return
}
@@ -585,7 +655,7 @@ func SubmitPullReview(ctx *context.APIContext) {
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
if err != nil {
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -616,7 +686,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
case api.ReviewStateApproved:
// can not approve your own PR
if pr.Issue.IsPoster(ctx.Doer.ID) {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("approve your own pull is not allowed"))
ctx.APIError(http.StatusUnprocessableEntity, "approve your own pull is not allowed")
return -1, true
}
reviewType = issues_model.ReviewTypeApprove
@@ -625,7 +695,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
case api.ReviewStateRequestChanges:
// can not reject your own PR
if pr.Issue.IsPoster(ctx.Doer.ID) {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("reject your own pull is not allowed"))
ctx.APIError(http.StatusUnprocessableEntity, "reject your own pull is not allowed")
return -1, true
}
reviewType = issues_model.ReviewTypeReject
@@ -635,7 +705,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
needsBody = false
// if there is no body we need to ensure that there are comments
if !hasBody && !hasComments {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body or a comment", event))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body or a comment", event))
return -1, true
}
default:
@@ -644,7 +714,7 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
// reject reviews with empty body if a body is required for this call
if needsBody && !hasBody {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("review event %s requires a body", event))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("review event %s requires a body", event))
return -1, true
}
@@ -655,33 +725,25 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues_model.PullRequest, bool) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil, nil, true
}
review, err := issues_model.GetReviewByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrReviewNotExist(err) {
ctx.APIErrorNotFound("GetReviewByID", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil, nil, true
}
// validate the review is for the given PR
if review.IssueID != pr.IssueID {
ctx.APIErrorNotFound("ReviewNotInPR")
ctx.APIErrorNotFound()
return nil, nil, true
}
// make sure that the user has access to this review if it is pending
if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
ctx.APIErrorNotFound("GetReviewByID")
ctx.APIErrorNotFound()
return nil, nil, true
}
@@ -693,7 +755,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues
return review, pr, false
}
// CreateReviewRequests create review requests to an pull request
// CreateReviewRequests create review requests to a pull request
func CreateReviewRequests(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoCreatePullReviewRequests
// ---
@@ -734,7 +796,7 @@ func CreateReviewRequests(ctx *context.APIContext) {
apiReviewRequest(ctx, *opts, true)
}
// DeleteReviewRequests delete review requests to an pull request
// DeleteReviewRequests delete review requests to a pull request
func DeleteReviewRequests(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/requested_reviewers repository repoDeletePullReviewRequests
// ---
@@ -788,7 +850,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIErrorNotFound("UserNotExist", fmt.Sprintf("User '%s' not exist", r))
ctx.APIErrorNotFound("user doesn't exist: " + r)
return nil, nil
}
ctx.APIErrorInternal(err)
@@ -804,7 +866,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN
teamReviewer, err = organization.GetTeam(ctx, ctx.Repo.Owner.ID, t)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIErrorNotFound("TeamNotExist", fmt.Sprintf("Team '%s' not exist", t))
ctx.APIErrorNotFound("team doesn't exist: " + t)
return nil, nil
}
ctx.APIErrorInternal(err)
@@ -820,11 +882,7 @@ func parseReviewersByNames(ctx *context.APIContext, reviewerNames, teamReviewerN
func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions, isAdd bool) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
ctx.APIErrorNotFound("GetPullRequestByIndex", err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return
}
@@ -853,11 +911,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
comment, err := issue_service.ReviewRequest(ctx, pr.Issue, ctx.Doer, &permDoer, reviewer, isAdd)
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
return
}
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -878,11 +936,11 @@ func apiReviewRequest(ctx *context.APIContext, opts api.PullReviewRequestOptions
comment, err := issue_service.TeamReviewRequest(ctx, pr.Issue, ctx.Doer, teamReviewer, isAdd)
if err != nil {
if issues_model.IsErrReviewRequestOnClosedPR(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
return
}
if issues_model.IsErrNotValidReviewRequest(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -1003,7 +1061,7 @@ func UnDismissPullReview(ctx *context.APIContext) {
}
func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) {
if !ctx.Repo.IsAdmin() {
if !ctx.Repo.Permission.IsAdmin() {
ctx.APIError(http.StatusForbidden, "Must be repo admin")
return
}
@@ -1020,7 +1078,7 @@ func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors
_, err := pull_service.DismissReview(ctx, review.ID, ctx.Repo.Repository.ID, msg, ctx.Doer, isDismiss, dismissPriors)
if err != nil {
if pull_service.IsErrDismissRequestOnClosedPR(err) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -4,25 +4,23 @@
package repo
import (
"errors"
"fmt"
"net/http"
auth_model "code.gitea.io/gitea/models/auth"
"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/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
"gitea.dev/modules/git"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
release_service "gitea.dev/services/release"
)
func canAccessReleaseDraft(ctx *context.APIContext) bool {
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit.TypeReleases) {
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit.TypeReleases) {
return false
}
if ctx.Data["IsApiToken"] != true {
@@ -243,7 +241,7 @@ func CreateRelease(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateReleaseOption)
if ctx.Repo.Repository.IsEmpty {
ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty"))
ctx.APIError(http.StatusUnprocessableEntity, "repo is empty")
return
}
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
@@ -273,11 +271,11 @@ func CreateRelease(ctx *context.APIContext) {
// It doesn't need to be the same as the "release note"
if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil {
if repo_model.IsErrReleaseAlreadyExist(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
} else if release_service.IsErrProtectedTagName(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if git.IsErrNotExist(err) {
ctx.APIError(http.StatusNotFound, fmt.Errorf("target \"%v\" not found: %w", rel.Target, err))
ctx.APIError(http.StatusNotFound, "target not found")
} else {
ctx.APIErrorInternal(err)
}
@@ -8,16 +8,16 @@ import (
"net/http"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"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/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
attachment_service "gitea.dev/services/attachment"
"gitea.dev/services/context"
"gitea.dev/services/context/upload"
"gitea.dev/services/convert"
)
func checkReleaseMatchRepo(ctx *context.APIContext, releaseID int64) bool {
@@ -205,7 +205,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
// Check if attachments are enabled
if !setting.Attachment.Enabled {
ctx.APIErrorNotFound("Attachment is not enabled")
ctx.APIErrorNotFound("attachment is not enabled")
return
}
@@ -250,12 +250,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
ctx.APIError(http.StatusRequestEntityTooLarge, err.Error())
return
}
@@ -340,7 +340,7 @@ func EditReleaseAttachment(ctx *context.APIContext) {
if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -6,11 +6,11 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
"gitea.dev/services/context"
"gitea.dev/services/convert"
release_service "gitea.dev/services/release"
)
// GetReleaseByTag get a single release of a repository by tag name
@@ -60,7 +60,7 @@ func GetReleaseByTag(ctx *context.APIContext) {
}
if release.IsDraft { // only the users with write access can see draft releases
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit_model.TypeReleases) {
if !ctx.IsSigned || !ctx.Repo.Permission.CanWrite(unit_model.TypeReleases) {
ctx.APIErrorNotFound()
return
}
+117 -62
View File
@@ -13,31 +13,34 @@ import (
"strings"
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"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"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/label"
"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"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
feed_service "code.gitea.io/gitea/services/feed"
"code.gitea.io/gitea/services/issue"
repo_service "code.gitea.io/gitea/services/repository"
activities_model "gitea.dev/models/activities"
"gitea.dev/models/db"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
unit_model "gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/label"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
repo_module "gitea.dev/modules/repository"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/validation"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
"gitea.dev/services/convert"
feed_service "gitea.dev/services/feed"
"gitea.dev/services/issue"
"gitea.dev/services/migrations"
mirror_service "gitea.dev/services/mirror"
repo_service "gitea.dev/services/repository"
)
// Search repositories via options
@@ -170,7 +173,7 @@ func Search(ctx *context.APIContext) {
opts.Collaborate = optional.Some(true)
case "":
default:
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid search mode: \"%s\"", mode))
ctx.APIError(http.StatusUnprocessableEntity, "invalid search mode")
return
}
@@ -182,24 +185,11 @@ func Search(ctx *context.APIContext) {
opts.IsPrivate = optional.Some(ctx.FormBool("is_private"))
}
sortMode := ctx.FormString("sort")
if len(sortMode) > 0 {
sortOrder := ctx.FormString("order")
if len(sortOrder) == 0 {
sortOrder = "asc"
}
if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
return
}
} else {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
return
}
orderBy, ok := utils.ResolveSortOrder(ctx, repo_model.OrderByMap, "")
if !ok {
return
}
opts.OrderBy = orderBy
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
@@ -244,7 +234,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
// If the readme template does not exist, a 400 will be returned.
if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) {
ctx.APIError(http.StatusBadRequest, fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes))
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("readme template does not exist, available templates: %v", repo_module.Readmes))
return
}
@@ -260,7 +250,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
DefaultBranch: opt.DefaultBranch,
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
IsTemplate: opt.Template,
ObjectFormatName: opt.ObjectFormatName,
ObjectFormatName: string(opt.ObjectFormatName),
})
if err != nil {
if repo_model.IsErrRepoAlreadyExist(err) {
@@ -268,9 +258,9 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
} else if db.IsErrNameReserved(err) ||
db.IsErrNamePatternNotAllowed(err) ||
label.IsErrTemplateLoad(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -423,7 +413,7 @@ func Generate(ctx *context.APIContext) {
ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
} else if db.IsErrNameReserved(err) ||
db.IsErrNamePatternNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -630,7 +620,11 @@ func Edit(ctx *context.APIContext) {
}
}
if opts.MirrorInterval != nil || opts.EnablePrune != nil {
if opts.MirrorInterval != nil ||
opts.EnablePrune != nil ||
opts.MirrorUsername != nil ||
opts.MirrorPassword != nil ||
opts.MirrorToken != nil {
if err := updateMirror(ctx, opts); err != nil {
return
}
@@ -658,13 +652,13 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
switch {
case repo_model.IsErrRepoAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case db.IsErrNameReserved(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case db.IsErrNamePatternNotAllowed(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
default:
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("ChangeRepositoryName: %w", err))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("ChangeRepositoryName: %v", err))
}
return err
}
@@ -698,7 +692,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
err := errors.New("cannot change private repository to public")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -760,14 +754,14 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if opts.HasIssues != nil {
if *opts.HasIssues && opts.ExternalTracker != nil && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
if !validation.IsValidURL(opts.ExternalTracker.ExternalTrackerURL) {
err := errors.New("External tracker URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
err := errors.New("External tracker URL format not valid")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -822,7 +816,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
if opts.HasWiki != nil {
if *opts.HasWiki && opts.ExternalWiki != nil && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
// Check that values are valid
if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
if !validation.IsValidURL(opts.ExternalWiki.ExternalWikiURL) {
err := errors.New("External wiki URL not valid")
ctx.APIError(http.StatusUnprocessableEntity, "Invalid external wiki URL")
return err
@@ -893,10 +887,20 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
optional.AssignPtrValue(changed, &config.AllowFastForwardOnly, opts.AllowFastForwardOnly)
optional.AssignPtrValue(changed, &config.AllowManualMerge, opts.AllowManualMerge)
optional.AssignPtrValue(changed, &config.AutodetectManualMerge, opts.AutodetectManualMerge)
optional.AssignPtrValue(changed, &config.AllowMergeUpdate, opts.AllowMergeUpdate)
optional.AssignPtrValue(changed, &config.AllowRebaseUpdate, opts.AllowRebaseUpdate)
optional.AssignPtrValue(changed, &config.DefaultDeleteBranchAfterMerge, opts.DefaultDeleteBranchAfterMerge)
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
optional.AssignPtrString(changed, &config.DefaultUpdateStyle, opts.DefaultUpdateStyle)
// only validate update-style fields when the caller is actually changing one of them,
// so unrelated PATCH calls don't reject historical configs.
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
if err := config.ValidateUpdateSettings(); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
}
if *changed || mustInsertPullRequestUnit {
units = append(units, repo_model.RepoUnit{
RepoID: repo.ID,
@@ -984,7 +988,7 @@ func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) e
if opts.Archived != nil {
if repo.IsMirror {
err := errors.New("repo is a mirror, cannot archive/un-archive")
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
if *opts.Archived {
@@ -1038,14 +1042,14 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
interval, err := time.ParseDuration(*opts.MirrorInterval)
if err != nil {
log.Error("Wrong format for MirrorInternal Sent: %s", err)
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
// Ensure the provided duration is not too short
if interval != 0 && interval < setting.Mirror.MinInterval {
err := fmt.Errorf("invalid mirror interval: %s is below minimum interval: %s", interval, setting.Mirror.MinInterval)
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -1061,10 +1065,61 @@ func updateMirror(ctx *context.APIContext, opts api.EditRepoOption) error {
log.Trace("Repository %s Mirror[%d] Set EnablePrune: %t", repo.FullName(), mirror.ID, mirror.EnablePrune)
}
authUpdateRequested := opts.MirrorPassword != nil || opts.MirrorToken != nil || opts.MirrorUsername != nil
if authUpdateRequested {
remoteURL, err := gitrepo.GitRemoteGetURL(ctx, repo, mirror.GetRemoteName())
if err != nil {
ctx.APIErrorInternal(err)
return err
}
authUsername := ""
if opts.MirrorUsername != nil {
authUsername = *opts.MirrorUsername
} else if remoteURL.User != nil {
authUsername = remoteURL.User.Username()
}
authPassword := ""
authToken := ""
if opts.MirrorPassword != nil {
authPassword = *opts.MirrorPassword
}
if opts.MirrorToken != nil {
authToken = *opts.MirrorToken
}
if opts.MirrorPassword == nil && opts.MirrorToken == nil && remoteURL.User != nil && (authUsername == "" || authUsername == remoteURL.User.Username()) {
authPassword, _ = remoteURL.User.Password()
}
if authToken != "" {
authPassword = authToken
}
composedAddress, err := git.ParseRemoteAddr(repo.OriginalURL, authUsername, authPassword)
if err == nil {
err = migrations.IsMigrateURLAllowed(composedAddress, ctx.Doer)
}
if err != nil {
handleRemoteAddrError(ctx, err)
return err
}
if err := mirror_service.UpdateAddress(ctx, mirror, composedAddress); err != nil {
ctx.APIErrorInternal(err)
return err
}
if sanitized, err := util.SanitizeURL(repo.OriginalURL); err == nil {
mirror.RemoteAddress = sanitized
}
}
// finally update the mirror in the DB
if err := repo_model.UpdateMirror(ctx, mirror); err != nil {
log.Error("Failed to Set Mirror Interval: %s", err)
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return err
}
@@ -7,11 +7,11 @@ import (
"net/http"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/contexttest"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/services/contexttest"
"github.com/stretchr/testify/assert"
)
@@ -6,11 +6,11 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_model "gitea.dev/models/repo"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListStargazers list a repository's stargazers
@@ -7,14 +7,14 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
commitstatus_service "gitea.dev/services/repository/commitstatus"
)
// NewCommitStatus creates a new CommitStatus
@@ -55,7 +55,7 @@ func NewCommitStatus(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateStatusOption)
sha := ctx.PathParam("sha")
if len(sha) == 0 {
ctx.APIError(http.StatusBadRequest, nil)
ctx.APIError(http.StatusBadRequest, "sha not provided")
return
}
status := &git_model.CommitStatus{
@@ -6,11 +6,11 @@ package repo
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_model "gitea.dev/models/repo"
api "gitea.dev/modules/structs"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListSubscribers list a repo's subscribers (i.e. watchers)
+19 -19
View File
@@ -9,16 +9,16 @@ import (
"net/http"
"strings"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
git_model "gitea.dev/models/git"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
release_service "gitea.dev/services/release"
)
// ListTags list all the tags of a repository
@@ -109,13 +109,13 @@ func GetAnnotatedTag(ctx *context.APIContext) {
tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
commit, err := ctx.Repo.GitRepo.GetTagCommit(tag.Name)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx, ctx.Repo.Repository, tag, commit))
@@ -203,13 +203,13 @@ func CreateTag(ctx *context.APIContext) {
commit, err := ctx.Repo.GitRepo.GetCommit(form.Target)
if err != nil {
ctx.APIError(http.StatusNotFound, fmt.Errorf("target not found: %w", err))
ctx.APIError(http.StatusNotFound, fmt.Sprintf("target not found: %v", err))
return
}
if err := release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, commit.ID.String(), form.TagName, form.Message); err != nil {
if release_service.IsErrTagAlreadyExists(err) {
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
return
}
if release_service.IsErrProtectedTagName(err) {
@@ -278,7 +278,7 @@ func DeleteTag(ctx *context.APIContext) {
}
if !tag.IsTag {
ctx.APIError(http.StatusConflict, errors.New("a tag attached to a release cannot be deleted directly"))
ctx.APIError(http.StatusConflict, "a tag attached to a release cannot be deleted directly")
return
}
@@ -438,7 +438,7 @@ func CreateTagProtection(ctx *context.APIContext) {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -449,7 +449,7 @@ func CreateTagProtection(ctx *context.APIContext) {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -546,7 +546,7 @@ func EditTagProtection(ctx *context.APIContext) {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -560,7 +560,7 @@ func EditTagProtection(ctx *context.APIContext) {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorInternal(err)
@@ -7,10 +7,10 @@ import (
"fmt"
"net/http"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/organization"
"gitea.dev/services/context"
"gitea.dev/services/convert"
repo_service "gitea.dev/services/repository"
)
// ListTeams list a repository's teams
@@ -137,6 +137,8 @@ func AddTeam(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "405":
@@ -173,6 +175,8 @@ func DeleteTeam(ctx *context.APIContext) {
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"
// "405":
@@ -186,9 +190,9 @@ func DeleteTeam(ctx *context.APIContext) {
func changeRepoTeam(ctx *context.APIContext, add bool) {
if !ctx.Repo.Owner.IsOrganization() {
ctx.APIError(http.StatusMethodNotAllowed, "repo is not owned by an organization")
return
}
if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() {
ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner")
if !canChangeRepoTeam(ctx) {
return
}
@@ -201,13 +205,13 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
var err error
if add {
if repoHasTeam {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' is already added to repo", team.Name))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team '%s' is already added to repo", team.Name))
return
}
err = repo_service.TeamAddRepository(ctx, team, ctx.Repo.Repository)
} else {
if !repoHasTeam {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team '%s' was not added to repo", team.Name))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team '%s' was not added to repo", team.Name))
return
}
err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID)
@@ -220,11 +224,24 @@ func changeRepoTeam(ctx *context.APIContext, add bool) {
ctx.Status(http.StatusNoContent)
}
func canChangeRepoTeam(ctx *context.APIContext) bool {
canChange, err := organization.OrgFromUser(ctx.Repo.Owner).CanChangeRepoTeamAccess(ctx, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return false
}
if !canChange {
ctx.APIError(http.StatusForbidden, "Must be an organization owner")
return false
}
return true
}
func getTeamByParam(ctx *context.APIContext) *organization.Team {
team, err := organization.GetTeam(ctx, ctx.Repo.Owner.ID, ctx.PathParam("team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
return nil
}
ctx.APIErrorInternal(err)
@@ -7,14 +7,14 @@ import (
"net/http"
"strings"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// ListTopics returns list of current topics for repo
@@ -300,7 +300,7 @@ func TopicSearch(ctx *context.APIContext) {
}
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, map[string]any{
"topics": topicResponses,
ctx.JSON(http.StatusOK, api.TopicListResponse{
Topics: topicResponses,
})
}
@@ -8,18 +8,18 @@ import (
"fmt"
"net/http"
"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"
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"
repo_service "code.gitea.io/gitea/services/repository"
"gitea.dev/models/organization"
"gitea.dev/models/perm"
access_model "gitea.dev/models/perm/access"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
repo_service "gitea.dev/services/repository"
)
// Transfer transfers the ownership of a repository
@@ -87,12 +87,12 @@ func Transfer(ctx *context.APIContext) {
for _, tID := range *opts.TeamIDs {
team, err := organization.GetTeamByID(ctx, tID)
if err != nil {
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("team %d not found", tID))
ctx.APIError(http.StatusUnprocessableEntity, fmt.Sprintf("team %d not found", tID))
return
}
if team.OrgID != org.ID {
ctx.APIError(http.StatusForbidden, fmt.Errorf("team %d belongs not to org %d", tID, org.ID))
ctx.APIError(http.StatusForbidden, fmt.Sprintf("team %d belongs not to org %d", tID, org.ID))
return
}
@@ -110,13 +110,13 @@ func Transfer(ctx *context.APIContext) {
if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, ctx.Repo.Repository, teams); err != nil {
switch {
case repo_model.IsErrRepoTransferInProgress(err):
ctx.APIError(http.StatusConflict, err)
ctx.APIError(http.StatusConflict, err.Error())
case repo_model.IsErrRepoAlreadyExist(err):
ctx.APIError(http.StatusUnprocessableEntity, err)
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
case repo_service.IsRepositoryLimitReached(err):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
case errors.Is(err, user_model.ErrBlockedUser):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -163,11 +163,11 @@ func AcceptTransfer(ctx *context.APIContext) {
if err != nil {
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
case repo_service.IsRepositoryLimitReached(err):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -207,9 +207,9 @@ func RejectTransfer(ctx *context.APIContext) {
if err != nil {
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.APIError(http.StatusNotFound, err)
ctx.APIError(http.StatusNotFound, err.Error())
case errors.Is(err, util.ErrPermissionDenied):
ctx.APIError(http.StatusForbidden, err)
ctx.APIError(http.StatusForbidden, err.Error())
default:
ctx.APIErrorInternal(err)
}
@@ -6,8 +6,8 @@ package repo
import (
"net/http"
"code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
"gitea.dev/services/context"
files_service "gitea.dev/services/repository/files"
)
// GetTree get the tree of a repository.
@@ -9,17 +9,17 @@ import (
"net/http"
"net/url"
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"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
notify_service "code.gitea.io/gitea/services/notify"
wiki_service "code.gitea.io/gitea/services/wiki"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/gitrepo"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/convert"
notify_service "gitea.dev/services/notify"
wiki_service "gitea.dev/services/wiki"
)
// NewWikiPage response for wiki create request
@@ -59,7 +59,7 @@ func NewWikiPage(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
if util.IsEmptyString(form.Title) {
ctx.APIError(http.StatusBadRequest, nil)
ctx.APIError(http.StatusBadRequest, "title is required")
return
}
@@ -71,16 +71,16 @@ func NewWikiPage(ctx *context.APIContext) {
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
form.ContentBase64 = string(content)
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil {
if repo_model.IsErrWikiReservedName(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -149,7 +149,7 @@ func EditWikiPage(ctx *context.APIContext) {
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
form.ContentBase64 = string(content)
@@ -245,11 +245,7 @@ func DeleteWikiPage(ctx *context.APIContext) {
wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("pageName"))
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
if err.Error() == "file does not exist" {
ctx.APIErrorNotFound(err)
return
}
ctx.APIErrorInternal(err)
ctx.APIErrorAuto(err)
return
}
@@ -435,7 +431,7 @@ func ListPageRevisions(ctx *context.APIContext) {
page := max(ctx.FormInt("page"), 1)
// get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange(
commitsHistory, _, err := wikiRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
@@ -474,21 +470,13 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) {
wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
if git.IsErrNotExist(err) || err.Error() == "no such file or directory" {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil, nil
}
commit, err := wikiRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if err != nil {
if git.IsErrNotExist(err) {
ctx.APIErrorNotFound(err)
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return wikiRepo, nil
}
return wikiRepo, commit
@@ -6,9 +6,9 @@ package settings
import (
"net/http"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
)
// GetGeneralUISettings returns instance's global settings for ui
@@ -7,19 +7,35 @@ import (
"fmt"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"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/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/webhook"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
"xorm.io/builder"
)
// actionsOwnerAccessibleRepoIDsSubQuery returns the sub-query restricting an owner-scoped actions
// listing to the repos whose actions the caller can read, or nil when no restriction applies. A bare
// org member must not be able to enumerate runs/jobs of repos they have no access to. A site admin may
// skip the access filter, but a public-only token must stay confined to public repos even for an admin.
func actionsOwnerAccessibleRepoIDsSubQuery(ctx *context.APIContext, ownerID int64) *builder.Builder {
if ownerID > 0 && (ctx.Doer == nil || !ctx.Doer.IsAdmin || ctx.PublicOnly) {
return repo_model.FindUserActionsAccessibleOwnerRepoIDsSubQuery(ownerID, ctx.Doer, ctx.PublicOnly)
}
return nil
}
// ListJobs lists jobs for api route validated ownerID and repoID
// ownerID == 0 and repoID == 0 means all jobs
// ownerID == 0 and repoID != 0 means all jobs for the given repo
@@ -27,27 +43,38 @@ import (
// ownerID != 0 and repoID != 0 undefined behavior
// runID == 0 means all jobs
// runID is used as an additional filter together with ownerID and repoID to only return jobs for the given run
// runAttemptID, when set, additionally limits the result to jobs of the specified run attempt. Only takes effect when runID > 0.
// Access rights are checked at the API route level
func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptID optional.Option[int64]) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
listOptions := utils.GetListOptions(ctx)
orderBy, ok := utils.ResolveSortOrder(ctx, actions_model.JobOrderByMap, actions_model.JobOrderByMap["asc"]["id"])
if !ok {
return
}
opts := actions_model.FindRunJobOptions{
OwnerID: ownerID,
RepoID: repoID,
RunID: runID,
ListOptions: listOptions,
OrderBy: orderBy,
}
if runID > 0 {
opts.RunAttemptID = runAttemptID
}
for _, status := range ctx.FormStrings("status") {
values, err := convertToInternal(status)
if err != nil {
ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status))
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
opts.Statuses = append(opts.Statuses, values...)
}
opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID)
jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts)
if err != nil {
ctx.APIErrorInternal(err)
@@ -57,6 +84,12 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
res := new(api.ActionWorkflowJobsResponse)
res.TotalCount = total
jobList := actions_model.ActionJobList(jobs)
if err := jobList.LoadAttributes(ctx, true); err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries = make([]*api.ActionWorkflowJob, len(jobs))
isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID
@@ -65,11 +98,11 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) {
if isRepoLevel {
repository = ctx.Repo.Repository
} else {
repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID)
if err != nil {
ctx.APIErrorInternal(err)
if jobs[i].Run == nil || jobs[i].Run.Repo == nil {
ctx.APIErrorInternal(fmt.Errorf("job %d is missing its run or repository", jobs[i].ID))
return
}
repository = jobs[i].Run.Repo
}
convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i])
@@ -108,7 +141,7 @@ func convertToInternal(s string) ([]actions_model.Status, error) {
case "cancelled", "timed_out":
return []actions_model.Status{actions_model.StatusCancelled}, nil
default:
return nil, fmt.Errorf("invalid status %s", s)
return nil, util.NewInvalidArgumentErrorf("invalid status %s", s)
}
}
@@ -117,8 +150,9 @@ func convertToInternal(s string) ([]actions_model.Status, error) {
// ownerID == 0 and repoID != 0 means all runs for the given repo
// ownerID != 0 and repoID == 0 means all runs for the given user/org
// ownerID != 0 and repoID != 0 undefined behavior
// workflowID filters runs by workflow file name (e.g. "build.yml"), empty means no filter
// Access rights are checked at the API route level
func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string) {
if ownerID != 0 && repoID != 0 {
setting.PanicInDevOrTesting("ownerID and repoID should not be both set")
}
@@ -126,8 +160,14 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
opts := actions_model.FindRunOptions{
OwnerID: ownerID,
RepoID: repoID,
WorkflowID: workflowID,
ListOptions: listOptions,
}
if workflowID != "" {
workflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id")
opts.IsScopedRun = optional.Some(workflowSourceRepoID > 0)
opts.WorkflowRepoID = workflowSourceRepoID
}
if event := ctx.FormString("event"); event != "" {
opts.TriggerEvent = webhook.HookEventType(event)
@@ -138,7 +178,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
for _, status := range ctx.FormStrings("status") {
values, err := convertToInternal(status)
if err != nil {
ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status))
ctx.APIError(http.StatusBadRequest, err.Error())
return
}
opts.Status = append(opts.Status, values...)
@@ -154,6 +194,9 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
if headSHA := ctx.FormString("head_sha"); headSHA != "" {
opts.CommitSHA = headSHA
}
excludePullRequests := ctx.FormBool("exclude_pull_requests")
opts.AccessibleRepoIDsSubQuery = actionsOwnerAccessibleRepoIDsSubQuery(ctx, opts.OwnerID)
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
if err != nil {
@@ -164,21 +207,28 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64) {
res := new(api.ActionWorkflowRunsResponse)
res.TotalCount = total
res.Entries = make([]*api.ActionWorkflowRun, len(runs))
isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID
for i := range runs {
var repository *repo_model.Repository
if isRepoLevel {
repository = ctx.Repo.Repository
} else {
repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
runList := actions_model.RunList(runs)
if err := runList.LoadTriggerUser(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i])
if err := runList.LoadRepos(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
repos := repo_model.RepositoryList(container.FilterSlice(runs, func(r *actions_model.ActionRun) (*repo_model.Repository, bool) {
return r.Repo, r.Repo != nil
}))
if err := repos.LoadOwners(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries = make([]*api.ActionWorkflowRun, len(runs))
for i := range runs {
// TODO: load run attempts in batch
convertedRun, err := convert.ToActionWorkflowRun(ctx, runs[i], nil, excludePullRequests)
if err != nil {
ctx.APIErrorInternal(err)
return
@@ -7,12 +7,13 @@ import (
"errors"
"net/http"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
user_service "code.gitea.io/gitea/services/user"
user_model "gitea.dev/models/user"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
user_service "gitea.dev/services/user"
)
func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
@@ -44,33 +45,30 @@ func ListBlocks(ctx *context.APIContext, blocker *user_model.User) {
func CheckUserBlock(ctx *context.APIContext, blocker *user_model.User) {
blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username"))
if err != nil {
ctx.APIErrorNotFound("GetUserByName", err)
ctx.APIErrorAuto(err)
return
}
status := http.StatusNotFound
blocking, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
if err != nil {
_, err = user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
if errors.Is(err, util.ErrNotExist) {
ctx.Status(http.StatusNotFound)
} else if err == nil {
ctx.Status(http.StatusNoContent)
} else {
ctx.APIErrorInternal(err)
return
}
if blocking != nil {
status = http.StatusNoContent
}
ctx.Status(status)
}
func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username"))
if err != nil {
ctx.APIErrorNotFound("GetUserByName", err)
ctx.APIErrorAuto(err)
return
}
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, ctx.FormString("note")); err != nil {
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -83,13 +81,13 @@ func BlockUser(ctx *context.APIContext, blocker *user_model.User) {
func UnblockUser(ctx *context.APIContext, doer, blocker *user_model.User) {
blockee, err := user_model.GetUserByName(ctx, ctx.PathParam("username"))
if err != nil {
ctx.APIErrorNotFound("GetUserByName", err)
ctx.APIErrorAuto(err)
return
}
if err := user_service.UnblockUser(ctx, doer, blocker, blockee); err != nil {
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
ctx.APIError(http.StatusBadRequest, err)
ctx.APIError(http.StatusBadRequest, err.Error())
} else {
ctx.APIErrorInternal(err)
}
@@ -7,15 +7,15 @@ import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/routers/api/v1/utils"
"gitea.dev/services/context"
"gitea.dev/services/convert"
)
// RegistrationToken is response related to registration token
@@ -77,11 +77,7 @@ func getRunnerByID(ctx *context.APIContext, ownerID, repoID, runnerID int64) (*a
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.APIErrorNotFound("Runner not found")
} else {
ctx.APIErrorInternal(err)
}
ctx.APIErrorAuto(err)
return nil, false
}
@@ -3,7 +3,7 @@
package swagger
import api "code.gitea.io/gitea/modules/structs"
import api "gitea.dev/modules/structs"
// SecretList
// swagger:response SecretList
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// ActivityFeedsList
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// OAuth2Application
@@ -20,3 +20,10 @@ type swaggerResponseAccessToken struct {
// in:body
Body api.AccessToken `json:"body"`
}
// CurrentAccessToken represents the currently authenticated access token.
// swagger:response CurrentAccessToken
type swaggerResponseCurrentAccessToken struct {
// in:body
Body api.CurrentAccessToken `json:"body"`
}
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// CronList
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// Issue
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// PublicKey
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// ServerVersion
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// NodeInfo
@@ -4,7 +4,7 @@
package swagger
import (
api "code.gitea.io/gitea/modules/structs"
api "gitea.dev/modules/structs"
)
// NotificationThread

Some files were not shown because too many files have changed in this diff Show More