package gitx
import (
"errors"
"fmt"
"github.com/go-git/go-git/v5/plumbing"
)
// Sentinel errors, one per failure class. Callers compare with errors.Is and
// map the class to a status code: ErrNotFound to 404, ErrStale to 409,
// ErrRefRejected to a hook rejection message, ErrTooLarge to 413.
//
// core's sentinels (ErrInvalidName, ErrInvalidPath, ErrInvalidDocID,
// ErrMalformedFrontmatter, ...) are reused verbatim wherever the failure is a
// domain-rule violation rather than a git one; this package adds only the
// classes core cannot know about.
var (
// ErrNotFound marks a missing space, revision, ref, path or object.
// Invalid names resolve here too, so a crafted name cannot distinguish
// "malformed" from "absent" by probing.
ErrNotFound = errors.New("not found")
// ErrExists marks a create that would clobber something: a space whose
// directory is already present, or a proposal branch already in use.
ErrExists = errors.New("already exists")
// ErrBadRev marks a revision string that is not a usable ref name or hex
// object id. It is distinct from ErrNotFound: this is "cannot be a
// revision", not "is not in this repository".
ErrBadRev = errors.New("invalid revision")
// ErrTooLarge marks a blob or a tree walk that exceeded its byte or entry
// budget. Nothing is truncated: a half-read markdown document would be
// indexed and served as if it were whole, so the read fails instead.
ErrTooLarge = errors.New("too large")
// ErrStale marks a merge whose base moved under it. Match with errors.Is
// and type-assert to *StaleError for the current approved head, which is
// what the caller returns in the 409.
ErrStale = errors.New("stale base")
// ErrRefRejected marks a ref update the refs rule forbids. This is what the
// update hook reports back to the pushing client.
ErrRefRejected = errors.New("ref update rejected")
// ErrRefRace marks a compare-and-swap ref update that lost to a concurrent
// writer (in practice a native receive-pack push) more times than the retry
// budget allows. The operation had no effect.
ErrRefRace = errors.New("ref changed concurrently")
// ErrUnsupportedEntry marks a tree entry the document model has no meaning
// for: a submodule, or a symlink occupying a document path. Skipping it
// silently would drop a document out of the index with no trace, which is
// the exact failure the service exists to prevent.
ErrUnsupportedEntry = errors.New("unsupported tree entry")
// ErrDuplicateDocID marks two documents carrying the same id in one tree.
// Global uniqueness is the registry's job, but a tree that already violates
// it makes the id-keyed merge ambiguous, so it is refused here too.
ErrDuplicateDocID = errors.New("duplicate document id")
// ErrUnsupportedChange marks a proposal carrying a change the merge model
// cannot express: a deletion, a rename, or an edit to a non-document path.
// Deletion and rename are human-push-only by design.
ErrUnsupportedChange = errors.New("unsupported proposal change")
)
// StaleReason names which of the staleness cases fired. It is carried on
// StaleError so the caller can explain the 409 rather than just returning it.
type StaleReason string
const (
// StaleBaseDetached means the proposal's base is no longer an ancestor of
// the approved head — the approved branch was rewritten under it.
StaleBaseDetached StaleReason = "base is not an ancestor of the approved head"
// StaleDocChanged means the document changed on the approved branch since
// the proposal's base.
StaleDocChanged StaleReason = "document changed on the approved branch since the base"
// StaleDocRemoved means the document existed at the base and no longer
// exists on the approved head.
StaleDocRemoved StaleReason = "document was removed from the approved branch since the base"
// StaleDocAppeared means a document with this id appeared on the approved
// branch after the base, so the proposal would silently overwrite it.
StaleDocAppeared StaleReason = "a document with this id appeared on the approved branch after the base"
// StalePathTaken means the proposal's new document targets a path already
// occupied on the approved head by a different document.
StalePathTaken StaleReason = "path is occupied on the approved branch by a different document"
)
// StaleError reports that a merge cannot proceed because the approved branch
// moved under the proposal. Head is the current approved head, which the caller
// hands back in the 409 so the agent can refetch and re-propose against it.
type StaleError struct {
Reason StaleReason
// DocID is the document that went stale. Empty for StaleBaseDetached,
// which is about the proposal as a whole.
DocID string
// Path is where the document sits on the approved head, or the contested
// path for StalePathTaken. Empty when the document is not on the head.
Path string
// Base is the proposal's base revision, Head the current approved head.
Base plumbing.Hash
Head plumbing.Hash
}
func (e *StaleError) Error() string {
if e.DocID == "" {
return fmt.Sprintf("gitx: stale base %s (approved head is %s): %s",
e.Base, e.Head, e.Reason)
}
if e.Path == "" {
return fmt.Sprintf("gitx: stale base %s for %s (approved head is %s): %s",
e.Base, e.DocID, e.Head, e.Reason)
}
return fmt.Sprintf("gitx: stale base %s for %s at %q (approved head is %s): %s",
e.Base, e.DocID, e.Path, e.Head, e.Reason)
}
// Is makes errors.Is(err, ErrStale) true for every StaleError, so callers can
// branch on the class and only type-assert when they need the head.
func (e *StaleError) Is(target error) bool { return target == ErrStale }