| 1 |
|
package gitx |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"errors" |
| 5 |
|
"fmt" |
| 6 |
|
|
| 7 |
|
"github.com/go-git/go-git/v5/plumbing" |
| 8 |
|
) |
| 9 |
|
|
| 10 |
|
// Sentinel errors, one per failure class. Callers compare with errors.Is and |
| 11 |
|
// map the class to a status code: ErrNotFound to 404, ErrStale to 409, |
| 12 |
|
// ErrRefRejected to a hook rejection message, ErrTooLarge to 413. |
| 13 |
|
// |
| 14 |
|
// core's sentinels (ErrInvalidName, ErrInvalidPath, ErrInvalidDocID, |
| 15 |
|
// ErrMalformedFrontmatter, ...) are reused verbatim wherever the failure is a |
| 16 |
|
// domain-rule violation rather than a git one; this package adds only the |
| 17 |
|
// classes core cannot know about. |
| 18 |
|
var ( |
| 19 |
|
// ErrNotFound marks a missing space, revision, ref, path or object. |
| 20 |
|
// Invalid names resolve here too, so a crafted name cannot distinguish |
| 21 |
|
// "malformed" from "absent" by probing. |
| 22 |
|
ErrNotFound = errors.New("not found") |
| 23 |
|
|
| 24 |
|
// ErrExists marks a create that would clobber something: a space whose |
| 25 |
|
// directory is already present, or a proposal branch already in use. |
| 26 |
|
ErrExists = errors.New("already exists") |
| 27 |
|
|
| 28 |
|
// ErrBadRev marks a revision string that is not a usable ref name or hex |
| 29 |
|
// object id. It is distinct from ErrNotFound: this is "cannot be a |
| 30 |
|
// revision", not "is not in this repository". |
| 31 |
|
ErrBadRev = errors.New("invalid revision") |
| 32 |
|
|
| 33 |
|
// ErrTooLarge marks a blob or a tree walk that exceeded its byte or entry |
| 34 |
|
// budget. Nothing is truncated: a half-read markdown document would be |
| 35 |
|
// indexed and served as if it were whole, so the read fails instead. |
| 36 |
|
ErrTooLarge = errors.New("too large") |
| 37 |
|
|
| 38 |
|
// ErrStale marks a merge whose base moved under it. Match with errors.Is |
| 39 |
|
// and type-assert to *StaleError for the current approved head, which is |
| 40 |
|
// what the caller returns in the 409. |
| 41 |
|
ErrStale = errors.New("stale base") |
| 42 |
|
|
| 43 |
|
// ErrRefRejected marks a ref update the refs rule forbids. This is what the |
| 44 |
|
// update hook reports back to the pushing client. |
| 45 |
|
ErrRefRejected = errors.New("ref update rejected") |
| 46 |
|
|
| 47 |
|
// ErrRefRace marks a compare-and-swap ref update that lost to a concurrent |
| 48 |
|
// writer (in practice a native receive-pack push) more times than the retry |
| 49 |
|
// budget allows. The operation had no effect. |
| 50 |
|
ErrRefRace = errors.New("ref changed concurrently") |
| 51 |
|
|
| 52 |
|
// ErrUnsupportedEntry marks a tree entry the document model has no meaning |
| 53 |
|
// for: a submodule, or a symlink occupying a document path. Skipping it |
| 54 |
|
// silently would drop a document out of the index with no trace, which is |
| 55 |
|
// the exact failure the service exists to prevent. |
| 56 |
|
ErrUnsupportedEntry = errors.New("unsupported tree entry") |
| 57 |
|
|
| 58 |
|
// ErrDuplicateDocID marks two documents carrying the same id in one tree. |
| 59 |
|
// Global uniqueness is the registry's job, but a tree that already violates |
| 60 |
|
// it makes the id-keyed merge ambiguous, so it is refused here too. |
| 61 |
|
ErrDuplicateDocID = errors.New("duplicate document id") |
| 62 |
|
|
| 63 |
|
// ErrUnsupportedChange marks a proposal carrying a change the merge model |
| 64 |
|
// cannot express: a deletion, a rename, or an edit to a non-document path. |
| 65 |
|
// Deletion and rename are human-push-only by design. |
| 66 |
|
ErrUnsupportedChange = errors.New("unsupported proposal change") |
| 67 |
|
) |
| 68 |
|
|
| 69 |
|
// StaleReason names which of the staleness cases fired. It is carried on |
| 70 |
|
// StaleError so the caller can explain the 409 rather than just returning it. |
| 71 |
|
type StaleReason string |
| 72 |
|
|
| 73 |
|
const ( |
| 74 |
|
// StaleBaseDetached means the proposal's base is no longer an ancestor of |
| 75 |
|
// the approved head — the approved branch was rewritten under it. |
| 76 |
|
StaleBaseDetached StaleReason = "base is not an ancestor of the approved head" |
| 77 |
|
|
| 78 |
|
// StaleDocChanged means the document changed on the approved branch since |
| 79 |
|
// the proposal's base. |
| 80 |
|
StaleDocChanged StaleReason = "document changed on the approved branch since the base" |
| 81 |
|
|
| 82 |
|
// StaleDocRemoved means the document existed at the base and no longer |
| 83 |
|
// exists on the approved head. |
| 84 |
|
StaleDocRemoved StaleReason = "document was removed from the approved branch since the base" |
| 85 |
|
|
| 86 |
|
// StaleDocAppeared means a document with this id appeared on the approved |
| 87 |
|
// branch after the base, so the proposal would silently overwrite it. |
| 88 |
|
StaleDocAppeared StaleReason = "a document with this id appeared on the approved branch after the base" |
| 89 |
|
|
| 90 |
|
// StalePathTaken means the proposal's new document targets a path already |
| 91 |
|
// occupied on the approved head by a different document. |
| 92 |
|
StalePathTaken StaleReason = "path is occupied on the approved branch by a different document" |
| 93 |
|
) |
| 94 |
|
|
| 95 |
|
// StaleError reports that a merge cannot proceed because the approved branch |
| 96 |
|
// moved under the proposal. Head is the current approved head, which the caller |
| 97 |
|
// hands back in the 409 so the agent can refetch and re-propose against it. |
| 98 |
|
type StaleError struct { |
| 99 |
|
Reason StaleReason |
| 100 |
|
// DocID is the document that went stale. Empty for StaleBaseDetached, |
| 101 |
|
// which is about the proposal as a whole. |
| 102 |
|
DocID string |
| 103 |
|
// Path is where the document sits on the approved head, or the contested |
| 104 |
|
// path for StalePathTaken. Empty when the document is not on the head. |
| 105 |
|
Path string |
| 106 |
|
// Base is the proposal's base revision, Head the current approved head. |
| 107 |
|
Base plumbing.Hash |
| 108 |
|
Head plumbing.Hash |
| 109 |
|
} |
| 110 |
|
|
| 111 |
0 |
func (e *StaleError) Error() string { |
| 112 |
0 |
if e.DocID == "" { |
| 113 |
0 |
return fmt.Sprintf("gitx: stale base %s (approved head is %s): %s", |
| 114 |
0 |
e.Base, e.Head, e.Reason) |
| 115 |
0 |
} |
| 116 |
0 |
if e.Path == "" { |
| 117 |
0 |
return fmt.Sprintf("gitx: stale base %s for %s (approved head is %s): %s", |
| 118 |
0 |
e.Base, e.DocID, e.Head, e.Reason) |
| 119 |
0 |
} |
| 120 |
0 |
return fmt.Sprintf("gitx: stale base %s for %s at %q (approved head is %s): %s", |
| 121 |
0 |
e.Base, e.DocID, e.Path, e.Head, e.Reason) |
| 122 |
|
} |
| 123 |
|
|
| 124 |
|
// Is makes errors.Is(err, ErrStale) true for every StaleError, so callers can |
| 125 |
|
// branch on the class and only type-assert when they need the head. |
| 126 |
1 |
func (e *StaleError) Is(target error) bool { return target == ErrStale } |