~bigbes/sr-ht-spec

ref: f42f81ca17c2a1a3da14002e77562b51df14b34d sr-ht-spec/gitx/errors.go -rw-r--r-- 5.5 KiB
f42f81ca — Eugene Blikh feat(web): line-numbered unified prose diff replaces the block cards (spec-by6.3.5) 13 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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 }