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 }