Phase 3: Organization Public Profile Page - Pinned repositories with groups - Public members display with roles - API endpoints for pinned repos and groups Phase 4: Gitea Pages Foundation - Landing page templates (simple, docs, product, portfolio) - Custom domain support with verification - YAML configuration parser (.gitea/landing.yaml) - Repository settings UI for pages Phase 5: Enhanced Wiki System with V2 API - Full CRUD operations via v2 API - Full-text search with WikiIndex table - Link graph visualization - Wiki health metrics (orphaned, dead links, outdated) - Designed for external AI plugin integration - Developer guide for .NET integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
313 lines
12 KiB
Go
313 lines
12 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package errors
|
|
|
|
import "net/http"
|
|
|
|
// ErrorCode represents a machine-readable error code
|
|
type ErrorCode string
|
|
|
|
// Authentication errors (AUTH_)
|
|
const (
|
|
AuthTokenMissing ErrorCode = "AUTH_TOKEN_MISSING"
|
|
AuthTokenInvalid ErrorCode = "AUTH_TOKEN_INVALID"
|
|
AuthTokenExpired ErrorCode = "AUTH_TOKEN_EXPIRED"
|
|
AuthScopeInsufficient ErrorCode = "AUTH_SCOPE_INSUFFICIENT"
|
|
Auth2FARequired ErrorCode = "AUTH_2FA_REQUIRED"
|
|
AuthInvalidCredentials ErrorCode = "AUTH_INVALID_CREDENTIALS"
|
|
)
|
|
|
|
// Permission errors (PERM_)
|
|
const (
|
|
PermRepoReadDenied ErrorCode = "PERM_REPO_READ_DENIED"
|
|
PermRepoWriteDenied ErrorCode = "PERM_REPO_WRITE_DENIED"
|
|
PermRepoAdminRequired ErrorCode = "PERM_REPO_ADMIN_REQUIRED"
|
|
PermOrgMemberRequired ErrorCode = "PERM_ORG_MEMBER_REQUIRED"
|
|
PermOrgAdminRequired ErrorCode = "PERM_ORG_ADMIN_REQUIRED"
|
|
PermActionDenied ErrorCode = "PERM_ACTION_DENIED"
|
|
)
|
|
|
|
// Repository errors (REPO_)
|
|
const (
|
|
RepoNotFound ErrorCode = "REPO_NOT_FOUND"
|
|
RepoArchived ErrorCode = "REPO_ARCHIVED"
|
|
RepoDisabled ErrorCode = "REPO_DISABLED"
|
|
RepoTransferPending ErrorCode = "REPO_TRANSFER_PENDING"
|
|
RepoEmpty ErrorCode = "REPO_EMPTY"
|
|
RepoAlreadyExists ErrorCode = "REPO_ALREADY_EXISTS"
|
|
)
|
|
|
|
// File errors (FILE_)
|
|
const (
|
|
FileNotFound ErrorCode = "FILE_NOT_FOUND"
|
|
FileTooLarge ErrorCode = "FILE_TOO_LARGE"
|
|
FileConflict ErrorCode = "FILE_CONFLICT"
|
|
FileBinary ErrorCode = "FILE_BINARY"
|
|
FileTypeError ErrorCode = "FILE_TYPE_NOT_ALLOWED"
|
|
)
|
|
|
|
// Git errors (GIT_)
|
|
const (
|
|
GitRefNotFound ErrorCode = "GIT_REF_NOT_FOUND"
|
|
GitMergeConflict ErrorCode = "GIT_MERGE_CONFLICT"
|
|
GitBranchNotFound ErrorCode = "GIT_BRANCH_NOT_FOUND"
|
|
GitTagNotFound ErrorCode = "GIT_TAG_NOT_FOUND"
|
|
GitCommitNotFound ErrorCode = "GIT_COMMIT_NOT_FOUND"
|
|
GitPushRejected ErrorCode = "GIT_PUSH_REJECTED"
|
|
)
|
|
|
|
// Rate limiting errors (RATE_)
|
|
const (
|
|
RateLimitExceeded ErrorCode = "RATE_LIMIT_EXCEEDED"
|
|
RateQuotaExhausted ErrorCode = "RATE_QUOTA_EXHAUSTED"
|
|
)
|
|
|
|
// Validation errors (VAL_)
|
|
const (
|
|
ValInvalidInput ErrorCode = "VAL_INVALID_INPUT"
|
|
ValMissingField ErrorCode = "VAL_MISSING_FIELD"
|
|
ValInvalidName ErrorCode = "VAL_INVALID_NAME"
|
|
ValNameTooLong ErrorCode = "VAL_NAME_TOO_LONG"
|
|
ValInvalidEmail ErrorCode = "VAL_INVALID_EMAIL"
|
|
ValDuplicateName ErrorCode = "VAL_DUPLICATE_NAME"
|
|
ValInvalidFormat ErrorCode = "VAL_INVALID_FORMAT"
|
|
ValidationFailed ErrorCode = "VALIDATION_FAILED"
|
|
)
|
|
|
|
// General errors
|
|
const (
|
|
InternalError ErrorCode = "INTERNAL_ERROR"
|
|
PermAccessDenied ErrorCode = "ACCESS_DENIED"
|
|
RefNotFound ErrorCode = "REF_NOT_FOUND"
|
|
)
|
|
|
|
// Upload errors (UPLOAD_)
|
|
const (
|
|
UploadSessionNotFound ErrorCode = "UPLOAD_SESSION_NOT_FOUND"
|
|
UploadSessionExpired ErrorCode = "UPLOAD_SESSION_EXPIRED"
|
|
UploadChunkInvalid ErrorCode = "UPLOAD_CHUNK_INVALID"
|
|
UploadChunkSizeMismatch ErrorCode = "UPLOAD_CHUNK_SIZE_MISMATCH"
|
|
UploadChecksumMismatch ErrorCode = "UPLOAD_CHECKSUM_MISMATCH"
|
|
UploadIncomplete ErrorCode = "UPLOAD_INCOMPLETE"
|
|
UploadFileTooLarge ErrorCode = "UPLOAD_FILE_TOO_LARGE"
|
|
)
|
|
|
|
// Resource errors (RESOURCE_)
|
|
const (
|
|
ResourceNotFound ErrorCode = "RESOURCE_NOT_FOUND"
|
|
ResourceConflict ErrorCode = "RESOURCE_CONFLICT"
|
|
ResourceGone ErrorCode = "RESOURCE_GONE"
|
|
)
|
|
|
|
// Server errors (SERVER_)
|
|
const (
|
|
ServerInternal ErrorCode = "SERVER_INTERNAL_ERROR"
|
|
ServerUnavailable ErrorCode = "SERVER_UNAVAILABLE"
|
|
ServerTimeout ErrorCode = "SERVER_TIMEOUT"
|
|
)
|
|
|
|
// User errors (USER_)
|
|
const (
|
|
UserNotFound ErrorCode = "USER_NOT_FOUND"
|
|
UserAlreadyExists ErrorCode = "USER_ALREADY_EXISTS"
|
|
UserInactive ErrorCode = "USER_INACTIVE"
|
|
UserProhibitLogin ErrorCode = "USER_PROHIBIT_LOGIN"
|
|
)
|
|
|
|
// Organization errors (ORG_)
|
|
const (
|
|
OrgNotFound ErrorCode = "ORG_NOT_FOUND"
|
|
OrgAlreadyExists ErrorCode = "ORG_ALREADY_EXISTS"
|
|
)
|
|
|
|
// Issue errors (ISSUE_)
|
|
const (
|
|
IssueNotFound ErrorCode = "ISSUE_NOT_FOUND"
|
|
IssueClosed ErrorCode = "ISSUE_CLOSED"
|
|
IssueLocked ErrorCode = "ISSUE_LOCKED"
|
|
)
|
|
|
|
// Pull Request errors (PR_)
|
|
const (
|
|
PRNotFound ErrorCode = "PR_NOT_FOUND"
|
|
PRAlreadyMerged ErrorCode = "PR_ALREADY_MERGED"
|
|
PRNotMergeable ErrorCode = "PR_NOT_MERGEABLE"
|
|
PRWorkInProgress ErrorCode = "PR_WORK_IN_PROGRESS"
|
|
)
|
|
|
|
// Release errors (RELEASE_)
|
|
const (
|
|
ReleaseNotFound ErrorCode = "RELEASE_NOT_FOUND"
|
|
ReleaseTagExists ErrorCode = "RELEASE_TAG_EXISTS"
|
|
ReleaseIsDraft ErrorCode = "RELEASE_IS_DRAFT"
|
|
)
|
|
|
|
// Webhook errors (WEBHOOK_)
|
|
const (
|
|
WebhookNotFound ErrorCode = "WEBHOOK_NOT_FOUND"
|
|
WebhookDeliveryFail ErrorCode = "WEBHOOK_DELIVERY_FAILED"
|
|
)
|
|
|
|
// Wiki errors (WIKI_)
|
|
const (
|
|
WikiPageNotFound ErrorCode = "WIKI_PAGE_NOT_FOUND"
|
|
WikiPageAlreadyExists ErrorCode = "WIKI_PAGE_ALREADY_EXISTS"
|
|
WikiReservedName ErrorCode = "WIKI_RESERVED_NAME"
|
|
WikiDisabled ErrorCode = "WIKI_DISABLED"
|
|
)
|
|
|
|
// errorInfo contains metadata about an error code
|
|
type errorInfo struct {
|
|
Message string
|
|
HTTPStatus int
|
|
}
|
|
|
|
// errorCatalog maps error codes to their metadata
|
|
var errorCatalog = map[ErrorCode]errorInfo{
|
|
// Auth errors
|
|
AuthTokenMissing: {"No authentication token provided", http.StatusUnauthorized},
|
|
AuthTokenInvalid: {"Token is malformed or invalid", http.StatusUnauthorized},
|
|
AuthTokenExpired: {"Token has expired", http.StatusUnauthorized},
|
|
AuthScopeInsufficient: {"Token lacks required scope", http.StatusForbidden},
|
|
Auth2FARequired: {"Two-factor authentication required", http.StatusUnauthorized},
|
|
AuthInvalidCredentials: {"Invalid username or password", http.StatusUnauthorized},
|
|
|
|
// Permission errors
|
|
PermRepoReadDenied: {"Cannot read repository", http.StatusForbidden},
|
|
PermRepoWriteDenied: {"Cannot write to repository", http.StatusForbidden},
|
|
PermRepoAdminRequired: {"Repository admin access required", http.StatusForbidden},
|
|
PermOrgMemberRequired: {"Must be organization member", http.StatusForbidden},
|
|
PermOrgAdminRequired: {"Organization admin access required", http.StatusForbidden},
|
|
PermActionDenied: {"Permission denied for this action", http.StatusForbidden},
|
|
|
|
// Repository errors
|
|
RepoNotFound: {"Repository does not exist", http.StatusNotFound},
|
|
RepoArchived: {"Repository is archived", http.StatusForbidden},
|
|
RepoDisabled: {"Repository is disabled", http.StatusForbidden},
|
|
RepoTransferPending: {"Repository has pending transfer", http.StatusConflict},
|
|
RepoEmpty: {"Repository is empty", http.StatusUnprocessableEntity},
|
|
RepoAlreadyExists: {"Repository already exists", http.StatusConflict},
|
|
|
|
// File errors
|
|
FileNotFound: {"File does not exist", http.StatusNotFound},
|
|
FileTooLarge: {"File exceeds size limit", http.StatusRequestEntityTooLarge},
|
|
FileConflict: {"File was modified (SHA mismatch)", http.StatusConflict},
|
|
FileBinary: {"Cannot perform text operation on binary file", http.StatusBadRequest},
|
|
FileTypeError: {"File type not allowed", http.StatusBadRequest},
|
|
|
|
// Git errors
|
|
GitRefNotFound: {"Git reference not found", http.StatusNotFound},
|
|
GitMergeConflict: {"Merge conflict detected", http.StatusConflict},
|
|
GitBranchNotFound: {"Branch not found", http.StatusNotFound},
|
|
GitTagNotFound: {"Tag not found", http.StatusNotFound},
|
|
GitCommitNotFound: {"Commit not found", http.StatusNotFound},
|
|
GitPushRejected: {"Push rejected", http.StatusForbidden},
|
|
|
|
// Rate limiting errors
|
|
RateLimitExceeded: {"API rate limit exceeded", http.StatusTooManyRequests},
|
|
RateQuotaExhausted: {"Rate quota exhausted", http.StatusTooManyRequests},
|
|
|
|
// Validation errors
|
|
ValInvalidInput: {"Invalid input provided", http.StatusBadRequest},
|
|
ValMissingField: {"Required field is missing", http.StatusBadRequest},
|
|
ValInvalidName: {"Name contains invalid characters", http.StatusBadRequest},
|
|
ValNameTooLong: {"Name exceeds maximum length", http.StatusBadRequest},
|
|
ValInvalidEmail: {"Invalid email address", http.StatusBadRequest},
|
|
ValDuplicateName: {"Name already exists", http.StatusConflict},
|
|
ValInvalidFormat: {"Invalid format", http.StatusBadRequest},
|
|
ValidationFailed: {"Validation failed", http.StatusBadRequest},
|
|
|
|
// General errors
|
|
InternalError: {"Internal server error", http.StatusInternalServerError},
|
|
PermAccessDenied: {"Access denied", http.StatusForbidden},
|
|
RefNotFound: {"Reference not found", http.StatusNotFound},
|
|
|
|
// Upload errors
|
|
UploadSessionNotFound: {"Upload session does not exist", http.StatusNotFound},
|
|
UploadSessionExpired: {"Upload session has expired", http.StatusGone},
|
|
UploadChunkInvalid: {"Chunk number out of range", http.StatusBadRequest},
|
|
UploadChunkSizeMismatch: {"Chunk size doesn't match expected", http.StatusBadRequest},
|
|
UploadChecksumMismatch: {"File checksum verification failed", http.StatusBadRequest},
|
|
UploadIncomplete: {"Not all chunks have been uploaded", http.StatusBadRequest},
|
|
UploadFileTooLarge: {"File exceeds maximum upload size", http.StatusRequestEntityTooLarge},
|
|
|
|
// Resource errors
|
|
ResourceNotFound: {"Resource not found", http.StatusNotFound},
|
|
ResourceConflict: {"Resource conflict", http.StatusConflict},
|
|
ResourceGone: {"Resource no longer available", http.StatusGone},
|
|
|
|
// Server errors
|
|
ServerInternal: {"Internal server error", http.StatusInternalServerError},
|
|
ServerUnavailable: {"Service temporarily unavailable", http.StatusServiceUnavailable},
|
|
ServerTimeout: {"Request timeout", http.StatusGatewayTimeout},
|
|
|
|
// User errors
|
|
UserNotFound: {"User not found", http.StatusNotFound},
|
|
UserAlreadyExists: {"User already exists", http.StatusConflict},
|
|
UserInactive: {"User account is inactive", http.StatusForbidden},
|
|
UserProhibitLogin: {"User is not allowed to login", http.StatusForbidden},
|
|
|
|
// Organization errors
|
|
OrgNotFound: {"Organization not found", http.StatusNotFound},
|
|
OrgAlreadyExists: {"Organization already exists", http.StatusConflict},
|
|
|
|
// Issue errors
|
|
IssueNotFound: {"Issue not found", http.StatusNotFound},
|
|
IssueClosed: {"Issue is closed", http.StatusUnprocessableEntity},
|
|
IssueLocked: {"Issue is locked", http.StatusForbidden},
|
|
|
|
// Pull Request errors
|
|
PRNotFound: {"Pull request not found", http.StatusNotFound},
|
|
PRAlreadyMerged: {"Pull request already merged", http.StatusConflict},
|
|
PRNotMergeable: {"Pull request is not mergeable", http.StatusConflict},
|
|
PRWorkInProgress: {"Pull request is marked as work in progress", http.StatusUnprocessableEntity},
|
|
|
|
// Release errors
|
|
ReleaseNotFound: {"Release not found", http.StatusNotFound},
|
|
ReleaseTagExists: {"Release tag already exists", http.StatusConflict},
|
|
ReleaseIsDraft: {"Release is a draft", http.StatusUnprocessableEntity},
|
|
|
|
// Webhook errors
|
|
WebhookNotFound: {"Webhook not found", http.StatusNotFound},
|
|
WebhookDeliveryFail: {"Webhook delivery failed", http.StatusBadGateway},
|
|
|
|
// Wiki errors
|
|
WikiPageNotFound: {"Wiki page not found", http.StatusNotFound},
|
|
WikiPageAlreadyExists: {"Wiki page already exists", http.StatusConflict},
|
|
WikiReservedName: {"Wiki page name is reserved", http.StatusBadRequest},
|
|
WikiDisabled: {"Wiki is disabled for this repository", http.StatusForbidden},
|
|
}
|
|
|
|
// Message returns the human-readable message for an error code
|
|
func (e ErrorCode) Message() string {
|
|
if info, ok := errorCatalog[e]; ok {
|
|
return info.Message
|
|
}
|
|
return string(e)
|
|
}
|
|
|
|
// HTTPStatus returns the HTTP status code for an error code
|
|
func (e ErrorCode) HTTPStatus() int {
|
|
if info, ok := errorCatalog[e]; ok {
|
|
return info.HTTPStatus
|
|
}
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
// String returns the error code as a string
|
|
func (e ErrorCode) String() string {
|
|
return string(e)
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e ErrorCode) Error() string {
|
|
return e.Message()
|
|
}
|
|
|
|
// IsValid returns true if the error code is registered in the catalog
|
|
func (e ErrorCode) IsValid() bool {
|
|
_, ok := errorCatalog[e]
|
|
return ok
|
|
}
|