detached b60fb76

feat(domain): add merge requests, repository settings and the signing key types

MRTitle and PlainText follow the input rules of ADR 0017 (valid UTF-8, no
control characters, no bidi override or isolate, length limits); a request
that breaks rules is rejected whole with the fields named. The squash commit
is built in the domain (author of the first commit, Co-authored-by trailers,
gpgsig header). Ports for the merge request store, the merge Git operations
and the commit signer are declared with their mocks.
commit
b60fb7665a9ef9872ea87342060b512f9353d6c0
author
brice · · 2026-10-10 16:49 UTC
parent
fef88c6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +2562 -1
M .mockery.yml +3 -0
@@ -15,3 +15,6 @@ packages:
1515 GitBackend:
1616 EventBus:
1717 RepoReader:
18+ MergeRequestStore:
19+ MergeGit:
20+ CommitSigner:
@@ -15,3 +15,6 @@ packages:
15 GitBackend:15 GitBackend:
16 EventBus:16 EventBus:
17 RepoReader:17 RepoReader:
18+ MergeRequestStore:
19+ MergeGit:
20+ CommitSigner:
M internal/domain/event.go +50 -0
@@ -111,6 +111,56 @@ type PushEvent struct {
111111 // EventKind implements Event.
112112 func (PushEvent) EventKind() string { return "repo.pushed" }
113113
114+// MergeRequestOpened is published when a merge request is opened.
115+type MergeRequestOpened struct {
116+ MergeRequest MergeRequest
117+ At time.Time
118+ // Visibility and CreatedAt are those of the repository when the event
119+ // was published, like PushEvent's.
120+ Visibility Visibility
121+ CreatedAt time.Time
122+}
123+
124+// EventKind implements Event.
125+func (MergeRequestOpened) EventKind() string { return "mr.opened" }
126+
127+// MergeRequestMerged is published when a merge request was merged, before the
128+// PushEvent of the target branch.
129+type MergeRequestMerged struct {
130+ MergeRequest MergeRequest
131+ At time.Time
132+ Visibility Visibility
133+ CreatedAt time.Time
134+}
135+
136+// EventKind implements Event.
137+func (MergeRequestMerged) EventKind() string { return "mr.merged" }
138+
139+// MergeRequestClosed is published when a merge request was closed without
140+// merging.
141+type MergeRequestClosed struct {
142+ MergeRequest MergeRequest
143+ At time.Time
144+ Visibility Visibility
145+ CreatedAt time.Time
146+}
147+
148+// EventKind implements Event.
149+func (MergeRequestClosed) EventKind() string { return "mr.closed" }
150+
151+// CommentAdded is published when a comment was added to a merge request.
152+type CommentAdded struct {
153+ Repo RepoName
154+ Number int64
155+ Comment Comment
156+ At time.Time
157+ Visibility Visibility
158+ CreatedAt time.Time
159+}
160+
161+// EventKind implements Event.
162+func (CommentAdded) EventKind() string { return "mr.comment_added" }
163+
114164 // EventGap tells a subscriber that it missed events because it was too slow;
115165 // it must refresh its whole view.
116166 type EventGap struct{}
@@ -111,6 +111,56 @@ type PushEvent struct {
111 // EventKind implements Event.111 // EventKind implements Event.
112 func (PushEvent) EventKind() string { return "repo.pushed" }112 func (PushEvent) EventKind() string { return "repo.pushed" }
113 113
114+// MergeRequestOpened is published when a merge request is opened.
115+type MergeRequestOpened struct {
116+ MergeRequest MergeRequest
117+ At time.Time
118+ // Visibility and CreatedAt are those of the repository when the event
119+ // was published, like PushEvent's.
120+ Visibility Visibility
121+ CreatedAt time.Time
122+}
123+
124+// EventKind implements Event.
125+func (MergeRequestOpened) EventKind() string { return "mr.opened" }
126+
127+// MergeRequestMerged is published when a merge request was merged, before the
128+// PushEvent of the target branch.
129+type MergeRequestMerged struct {
130+ MergeRequest MergeRequest
131+ At time.Time
132+ Visibility Visibility
133+ CreatedAt time.Time
134+}
135+
136+// EventKind implements Event.
137+func (MergeRequestMerged) EventKind() string { return "mr.merged" }
138+
139+// MergeRequestClosed is published when a merge request was closed without
140+// merging.
141+type MergeRequestClosed struct {
142+ MergeRequest MergeRequest
143+ At time.Time
144+ Visibility Visibility
145+ CreatedAt time.Time
146+}
147+
148+// EventKind implements Event.
149+func (MergeRequestClosed) EventKind() string { return "mr.closed" }
150+
151+// CommentAdded is published when a comment was added to a merge request.
152+type CommentAdded struct {
153+ Repo RepoName
154+ Number int64
155+ Comment Comment
156+ At time.Time
157+ Visibility Visibility
158+ CreatedAt time.Time
159+}
160+
161+// EventKind implements Event.
162+func (CommentAdded) EventKind() string { return "mr.comment_added" }
163+
114 // EventGap tells a subscriber that it missed events because it was too slow;164 // EventGap tells a subscriber that it missed events because it was too slow;
115 // it must refresh its whole view.165 // it must refresh its whole view.
116 type EventGap struct{}166 type EventGap struct{}
M internal/domain/git.go +16 -0
@@ -472,3 +472,19 @@ type DiffLimits struct {
472472 func DefaultDiffLimits() DiffLimits {
473473 return DiffLimits{MaxFiles: 300, MaxFileLines: 2000, MaxTotalLines: 20000, MaxTotalBytes: 8 << 20}
474474 }
475+
476+// ParseBranchName validates s as the short name of a branch ("feat/x"), with
477+// the syntax rules of ParseRevision, and rejects a full ref name, which would
478+// not be short.
479+func ParseBranchName(s string) (string, error) {
480+ switch {
481+ case s == "":
482+ return "", fmt.Errorf("%w: empty branch name", ErrInvalidRevision)
483+ case strings.HasPrefix(s, "refs/"):
484+ return "", fmt.Errorf("%w: %q: give the short name of the branch", ErrInvalidRevision, s)
485+ }
486+ if reason := refNameProblem(s); reason != "" {
487+ return "", fmt.Errorf("%w: %q: %s", ErrInvalidRevision, s, reason)
488+ }
489+ return s, nil
490+}
@@ -472,3 +472,19 @@ type DiffLimits struct {
472 func DefaultDiffLimits() DiffLimits {472 func DefaultDiffLimits() DiffLimits {
473 return DiffLimits{MaxFiles: 300, MaxFileLines: 2000, MaxTotalLines: 20000, MaxTotalBytes: 8 << 20}473 return DiffLimits{MaxFiles: 300, MaxFileLines: 2000, MaxTotalLines: 20000, MaxTotalBytes: 8 << 20}
474 }474 }
475+
476+// ParseBranchName validates s as the short name of a branch ("feat/x"), with
477+// the syntax rules of ParseRevision, and rejects a full ref name, which would
478+// not be short.
479+func ParseBranchName(s string) (string, error) {
480+ switch {
481+ case s == "":
482+ return "", fmt.Errorf("%w: empty branch name", ErrInvalidRevision)
483+ case strings.HasPrefix(s, "refs/"):
484+ return "", fmt.Errorf("%w: %q: give the short name of the branch", ErrInvalidRevision, s)
485+ }
486+ if reason := refNameProblem(s); reason != "" {
487+ return "", fmt.Errorf("%w: %q: %s", ErrInvalidRevision, s, reason)
488+ }
489+ return s, nil
490+}
A internal/domain/mergerequest.go +306 -0
@@ -0,0 +1,306 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "fmt"
7+ "slices"
8+ "strings"
9+ "time"
10+)
11+
12+// MergeMode is how a merge request is merged (ADR 0017).
13+type MergeMode string
14+
15+// Merge modes.
16+const (
17+ // MergeModeFastForward moves the target to the head; no commit is made.
18+ MergeModeFastForward MergeMode = "fast-forward"
19+ // MergeModeSquash makes one new signed commit with the tree of the head.
20+ MergeModeSquash MergeMode = "squash"
21+)
22+
23+// MergeModes returns the two modes in their canonical order.
24+func MergeModes() []MergeMode { return []MergeMode{MergeModeFastForward, MergeModeSquash} }
25+
26+// ParseMergeMode parses "fast-forward" or "squash".
27+func ParseMergeMode(s string) (MergeMode, error) {
28+ m := MergeMode(s)
29+ if slices.Contains(MergeModes(), m) {
30+ return m, nil
31+ }
32+ return "", fmt.Errorf("%w: merge mode %q (want fast-forward or squash)", ErrInvalid, s)
33+}
34+
35+// String returns the mode as text.
36+func (m MergeMode) String() string { return string(m) }
37+
38+// RepoSettings are the settings of a repository that govern its merge
39+// requests.
40+type RepoSettings struct {
41+ // AllowedMergeModes is never empty, in canonical order, without
42+ // duplicates.
43+ AllowedMergeModes []MergeMode
44+ // FailingChecksBlock says that a failing check prevents the merge.
45+ FailingChecksBlock bool
46+}
47+
48+// DefaultRepoSettings are the settings of a new repository: both modes, and
49+// failing checks block the merge.
50+func DefaultRepoSettings() RepoSettings {
51+ return RepoSettings{AllowedMergeModes: MergeModes(), FailingChecksBlock: true}
52+}
53+
54+// IsZero reports whether s holds no mode, which only the zero value does.
55+func (s RepoSettings) IsZero() bool { return len(s.AllowedMergeModes) == 0 }
56+
57+// Allows reports whether the mode may be used.
58+func (s RepoSettings) Allows(m MergeMode) bool { return slices.Contains(s.AllowedMergeModes, m) }
59+
60+// NormalizeMergeModes validates a list of modes for the settings: it must not
61+// be empty or hold an unknown mode. The result is in canonical order, without
62+// duplicates.
63+func NormalizeMergeModes(modes []MergeMode) ([]MergeMode, error) {
64+ if len(modes) == 0 {
65+ return nil, fmt.Errorf("%w: at least one merge mode is required", ErrInvalid)
66+ }
67+ for _, m := range modes {
68+ if _, err := ParseMergeMode(m.String()); err != nil {
69+ return nil, err
70+ }
71+ }
72+ var out []MergeMode
73+ for _, m := range MergeModes() {
74+ if slices.Contains(modes, m) {
75+ out = append(out, m)
76+ }
77+ }
78+ return out, nil
79+}
80+
81+// MRState is where a merge request is in its life.
82+type MRState string
83+
84+// Merge request states.
85+const (
86+ // MRStateOpen awaits review.
87+ MRStateOpen MRState = "open"
88+ // MRStateMerged is final: the branch was merged.
89+ MRStateMerged MRState = "merged"
90+ // MRStateClosed is final: the request was closed without merging.
91+ MRStateClosed MRState = "closed"
92+)
93+
94+// DiffSide is the side of a diff a comment is attached to.
95+type DiffSide string
96+
97+// Diff sides. The empty side marks a general comment.
98+const (
99+ // SideNone is the side of a general comment.
100+ SideNone DiffSide = ""
101+ // SideOld counts lines of the old file.
102+ SideOld DiffSide = "old"
103+ // SideNew counts lines of the new file.
104+ SideNew DiffSide = "new"
105+)
106+
107+// MergeRequest asks to merge one branch into another of the same repository.
108+type MergeRequest struct {
109+ Repo RepoName
110+ Number int64
111+ Title MRTitle
112+ // Description may be empty.
113+ Description PlainText
114+ // Source and Target are short branch names.
115+ Source, Target string
116+ State MRState
117+ // Author is the label of the token that opened it, "root" or "owner".
118+ Author string
119+ CreatedAt time.Time
120+ UpdatedAt time.Time
121+ // HeadID is the head of the source branch: stored when the request is
122+ // merged or closed, and filled from Git at every read while it is open
123+ // (zero when the branch is gone).
124+ HeadID ObjectID
125+ // BaseID is the merge base of the target and the head when the request
126+ // was merged or closed; zero while it is open.
127+ BaseID ObjectID
128+ // MergedCommitID is the tip of the target after the merge; zero unless
129+ // merged.
130+ MergedCommitID ObjectID
131+ // MergeMode is the mode used; empty unless merged.
132+ MergeMode MergeMode
133+ // CommentSeq is the id of the last comment of the request.
134+ CommentSeq int64
135+ // Version is set by the store on read: Update fails with ErrConflict when
136+ // the stored request changed since. Opaque to everyone else.
137+ Version uint64
138+}
139+
140+// IsOpen reports whether the request awaits review.
141+func (m MergeRequest) IsOpen() bool { return m.State == MRStateOpen }
142+
143+// Comment is a note on a merge request, general or on a line of its diff.
144+type Comment struct {
145+ // ID is unique within the merge request, in order of creation.
146+ ID int64
147+ Author string
148+ // Path, Line and Side are empty for a general comment.
149+ Path string
150+ Line int
151+ Side DiffSide
152+ // CommitID is the head of the request when the comment was written; zero
153+ // when the source branch was gone.
154+ CommitID ObjectID
155+ Text PlainText
156+ Resolved bool
157+ CreatedAt time.Time
158+ // Version is set by the store on read, like MergeRequest.Version.
159+ Version uint64
160+}
161+
162+// IsGeneral reports whether the comment is not attached to a line.
163+func (c Comment) IsGeneral() bool { return c.Path == "" }
164+
165+// HasLine reports whether the diff has a line numbered line on side in the
166+// file path: the new path of a file, or the old path of a deleted one. The new
167+// side holds the context and addition lines, the old side the context and
168+// deletion lines. A file the diff truncated has no lines.
169+func (d Diff) HasLine(path string, side DiffSide, line int) bool {
170+ if path == "" || line < 1 {
171+ return false
172+ }
173+ for _, f := range d.Files {
174+ name := f.NewPath
175+ if f.Status == FileDeleted {
176+ name = f.OldPath
177+ }
178+ if name != path {
179+ continue
180+ }
181+ for _, h := range f.Hunks {
182+ for _, l := range h.Lines {
183+ switch {
184+ case side == SideNew && l.Kind != LineDeletion && l.NewNumber == line:
185+ return true
186+ case side == SideOld && l.Kind != LineAddition && l.OldNumber == line:
187+ return true
188+ }
189+ }
190+ }
191+ }
192+ return false
193+}
194+
195+// Identity is a name and an email address, such as the committer of the
196+// commits ermite creates.
197+type Identity struct {
198+ Name, Email string
199+}
200+
201+// SquashCommit is the commit a squash merge creates: the tree of the head on
202+// top of the target.
203+type SquashCommit struct {
204+ Tree, Parent ObjectID
205+ Author, Committer Signature
206+ Message string
207+}
208+
209+// MaxSquashCommits bounds the commits a squash lists in its message.
210+const MaxSquashCommits = 1000
211+
212+// NewSquashCommit builds the squash commit of a merge request (ADR 0022).
213+// commits are the commits of the branch that the target lacks, newest first,
214+// as Git lists them, and not empty. The author is the author of the oldest
215+// one, with its date; the other distinct authors become Co-authored-by
216+// trailers, in order of appearance. The message is the title, " (!<number>)",
217+// a blank line, the summaries of the commits oldest first as a list, and the
218+// trailers.
219+func NewSquashCommit(mr MergeRequest, tree, parent ObjectID, commits []Commit, committer Identity, at time.Time) SquashCommit {
220+ oldest := slices.Clone(commits)
221+ slices.Reverse(oldest)
222+ var first Signature
223+ if len(oldest) > 0 {
224+ first = oldest[0].Author
225+ }
226+
227+ var msg strings.Builder
228+ fmt.Fprintf(&msg, "%s (!%d)\n", mr.Title.String(), mr.Number)
229+ if len(oldest) > 0 {
230+ msg.WriteString("\n")
231+ }
232+ var trailers []string
233+ seen := map[string]bool{strings.ToLower(first.Email): true}
234+ for _, c := range oldest {
235+ msg.WriteString("- " + singleLine(c.Summary) + "\n")
236+ key := strings.ToLower(c.Author.Email)
237+ if !seen[key] {
238+ seen[key] = true
239+ trailers = append(trailers, "Co-authored-by: "+identLine(c.Author.Name, c.Author.Email))
240+ }
241+ }
242+ if len(trailers) > 0 {
243+ msg.WriteString("\n" + strings.Join(trailers, "\n") + "\n")
244+ }
245+ return SquashCommit{
246+ Tree: tree, Parent: parent,
247+ Author: first,
248+ Committer: Signature{Name: committer.Name, Email: committer.Email, Time: at.UTC()},
249+ Message: msg.String(),
250+ }
251+}
252+
253+// Payload returns the text of the commit object without a signature, which is
254+// what the signature covers.
255+func (c SquashCommit) Payload() []byte { return c.encode("") }
256+
257+// Signed returns the text of the commit object with the armored SSH signature
258+// in its gpgsig header.
259+func (c SquashCommit) Signed(armored string) []byte { return c.encode(armored) }
260+
261+func (c SquashCommit) encode(armored string) []byte {
262+ var b strings.Builder
263+ b.WriteString("tree " + c.Tree.String() + "\n")
264+ b.WriteString("parent " + c.Parent.String() + "\n")
265+ b.WriteString("author " + signatureLine(c.Author) + "\n")
266+ b.WriteString("committer " + signatureLine(c.Committer) + "\n")
267+ if armored != "" {
268+ lines := strings.Split(strings.TrimRight(armored, "\n"), "\n")
269+ b.WriteString("gpgsig " + lines[0] + "\n")
270+ for _, l := range lines[1:] {
271+ b.WriteString(" " + l + "\n")
272+ }
273+ }
274+ b.WriteString("\n")
275+ b.WriteString(c.Message)
276+ return []byte(b.String())
277+}
278+
279+func signatureLine(s Signature) string {
280+ return fmt.Sprintf("%s %d +0000", identLine(s.Name, s.Email), s.Time.Unix())
281+}
282+
283+// identLine formats "Name <email>", dropping the characters Git does not
284+// allow in an identity.
285+func identLine(name, email string) string {
286+ clean := func(s string) string {
287+ return strings.TrimSpace(strings.Map(func(r rune) rune {
288+ if r == '<' || r == '>' || r < 0x20 || r == 0x7f {
289+ return -1
290+ }
291+ return r
292+ }, s))
293+ }
294+ return clean(name) + " <" + clean(email) + ">"
295+}
296+
297+// singleLine replaces the control characters of a commit summary, so that it
298+// stays one line of the message.
299+func singleLine(s string) string {
300+ return strings.Map(func(r rune) rune {
301+ if r < 0x20 || r == 0x7f {
302+ return ' '
303+ }
304+ return r
305+ }, s)
306+}
@@ -0,0 +1,306 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "fmt"
7+ "slices"
8+ "strings"
9+ "time"
10+)
11+
12+// MergeMode is how a merge request is merged (ADR 0017).
13+type MergeMode string
14+
15+// Merge modes.
16+const (
17+ // MergeModeFastForward moves the target to the head; no commit is made.
18+ MergeModeFastForward MergeMode = "fast-forward"
19+ // MergeModeSquash makes one new signed commit with the tree of the head.
20+ MergeModeSquash MergeMode = "squash"
21+)
22+
23+// MergeModes returns the two modes in their canonical order.
24+func MergeModes() []MergeMode { return []MergeMode{MergeModeFastForward, MergeModeSquash} }
25+
26+// ParseMergeMode parses "fast-forward" or "squash".
27+func ParseMergeMode(s string) (MergeMode, error) {
28+ m := MergeMode(s)
29+ if slices.Contains(MergeModes(), m) {
30+ return m, nil
31+ }
32+ return "", fmt.Errorf("%w: merge mode %q (want fast-forward or squash)", ErrInvalid, s)
33+}
34+
35+// String returns the mode as text.
36+func (m MergeMode) String() string { return string(m) }
37+
38+// RepoSettings are the settings of a repository that govern its merge
39+// requests.
40+type RepoSettings struct {
41+ // AllowedMergeModes is never empty, in canonical order, without
42+ // duplicates.
43+ AllowedMergeModes []MergeMode
44+ // FailingChecksBlock says that a failing check prevents the merge.
45+ FailingChecksBlock bool
46+}
47+
48+// DefaultRepoSettings are the settings of a new repository: both modes, and
49+// failing checks block the merge.
50+func DefaultRepoSettings() RepoSettings {
51+ return RepoSettings{AllowedMergeModes: MergeModes(), FailingChecksBlock: true}
52+}
53+
54+// IsZero reports whether s holds no mode, which only the zero value does.
55+func (s RepoSettings) IsZero() bool { return len(s.AllowedMergeModes) == 0 }
56+
57+// Allows reports whether the mode may be used.
58+func (s RepoSettings) Allows(m MergeMode) bool { return slices.Contains(s.AllowedMergeModes, m) }
59+
60+// NormalizeMergeModes validates a list of modes for the settings: it must not
61+// be empty or hold an unknown mode. The result is in canonical order, without
62+// duplicates.
63+func NormalizeMergeModes(modes []MergeMode) ([]MergeMode, error) {
64+ if len(modes) == 0 {
65+ return nil, fmt.Errorf("%w: at least one merge mode is required", ErrInvalid)
66+ }
67+ for _, m := range modes {
68+ if _, err := ParseMergeMode(m.String()); err != nil {
69+ return nil, err
70+ }
71+ }
72+ var out []MergeMode
73+ for _, m := range MergeModes() {
74+ if slices.Contains(modes, m) {
75+ out = append(out, m)
76+ }
77+ }
78+ return out, nil
79+}
80+
81+// MRState is where a merge request is in its life.
82+type MRState string
83+
84+// Merge request states.
85+const (
86+ // MRStateOpen awaits review.
87+ MRStateOpen MRState = "open"
88+ // MRStateMerged is final: the branch was merged.
89+ MRStateMerged MRState = "merged"
90+ // MRStateClosed is final: the request was closed without merging.
91+ MRStateClosed MRState = "closed"
92+)
93+
94+// DiffSide is the side of a diff a comment is attached to.
95+type DiffSide string
96+
97+// Diff sides. The empty side marks a general comment.
98+const (
99+ // SideNone is the side of a general comment.
100+ SideNone DiffSide = ""
101+ // SideOld counts lines of the old file.
102+ SideOld DiffSide = "old"
103+ // SideNew counts lines of the new file.
104+ SideNew DiffSide = "new"
105+)
106+
107+// MergeRequest asks to merge one branch into another of the same repository.
108+type MergeRequest struct {
109+ Repo RepoName
110+ Number int64
111+ Title MRTitle
112+ // Description may be empty.
113+ Description PlainText
114+ // Source and Target are short branch names.
115+ Source, Target string
116+ State MRState
117+ // Author is the label of the token that opened it, "root" or "owner".
118+ Author string
119+ CreatedAt time.Time
120+ UpdatedAt time.Time
121+ // HeadID is the head of the source branch: stored when the request is
122+ // merged or closed, and filled from Git at every read while it is open
123+ // (zero when the branch is gone).
124+ HeadID ObjectID
125+ // BaseID is the merge base of the target and the head when the request
126+ // was merged or closed; zero while it is open.
127+ BaseID ObjectID
128+ // MergedCommitID is the tip of the target after the merge; zero unless
129+ // merged.
130+ MergedCommitID ObjectID
131+ // MergeMode is the mode used; empty unless merged.
132+ MergeMode MergeMode
133+ // CommentSeq is the id of the last comment of the request.
134+ CommentSeq int64
135+ // Version is set by the store on read: Update fails with ErrConflict when
136+ // the stored request changed since. Opaque to everyone else.
137+ Version uint64
138+}
139+
140+// IsOpen reports whether the request awaits review.
141+func (m MergeRequest) IsOpen() bool { return m.State == MRStateOpen }
142+
143+// Comment is a note on a merge request, general or on a line of its diff.
144+type Comment struct {
145+ // ID is unique within the merge request, in order of creation.
146+ ID int64
147+ Author string
148+ // Path, Line and Side are empty for a general comment.
149+ Path string
150+ Line int
151+ Side DiffSide
152+ // CommitID is the head of the request when the comment was written; zero
153+ // when the source branch was gone.
154+ CommitID ObjectID
155+ Text PlainText
156+ Resolved bool
157+ CreatedAt time.Time
158+ // Version is set by the store on read, like MergeRequest.Version.
159+ Version uint64
160+}
161+
162+// IsGeneral reports whether the comment is not attached to a line.
163+func (c Comment) IsGeneral() bool { return c.Path == "" }
164+
165+// HasLine reports whether the diff has a line numbered line on side in the
166+// file path: the new path of a file, or the old path of a deleted one. The new
167+// side holds the context and addition lines, the old side the context and
168+// deletion lines. A file the diff truncated has no lines.
169+func (d Diff) HasLine(path string, side DiffSide, line int) bool {
170+ if path == "" || line < 1 {
171+ return false
172+ }
173+ for _, f := range d.Files {
174+ name := f.NewPath
175+ if f.Status == FileDeleted {
176+ name = f.OldPath
177+ }
178+ if name != path {
179+ continue
180+ }
181+ for _, h := range f.Hunks {
182+ for _, l := range h.Lines {
183+ switch {
184+ case side == SideNew && l.Kind != LineDeletion && l.NewNumber == line:
185+ return true
186+ case side == SideOld && l.Kind != LineAddition && l.OldNumber == line:
187+ return true
188+ }
189+ }
190+ }
191+ }
192+ return false
193+}
194+
195+// Identity is a name and an email address, such as the committer of the
196+// commits ermite creates.
197+type Identity struct {
198+ Name, Email string
199+}
200+
201+// SquashCommit is the commit a squash merge creates: the tree of the head on
202+// top of the target.
203+type SquashCommit struct {
204+ Tree, Parent ObjectID
205+ Author, Committer Signature
206+ Message string
207+}
208+
209+// MaxSquashCommits bounds the commits a squash lists in its message.
210+const MaxSquashCommits = 1000
211+
212+// NewSquashCommit builds the squash commit of a merge request (ADR 0022).
213+// commits are the commits of the branch that the target lacks, newest first,
214+// as Git lists them, and not empty. The author is the author of the oldest
215+// one, with its date; the other distinct authors become Co-authored-by
216+// trailers, in order of appearance. The message is the title, " (!<number>)",
217+// a blank line, the summaries of the commits oldest first as a list, and the
218+// trailers.
219+func NewSquashCommit(mr MergeRequest, tree, parent ObjectID, commits []Commit, committer Identity, at time.Time) SquashCommit {
220+ oldest := slices.Clone(commits)
221+ slices.Reverse(oldest)
222+ var first Signature
223+ if len(oldest) > 0 {
224+ first = oldest[0].Author
225+ }
226+
227+ var msg strings.Builder
228+ fmt.Fprintf(&msg, "%s (!%d)\n", mr.Title.String(), mr.Number)
229+ if len(oldest) > 0 {
230+ msg.WriteString("\n")
231+ }
232+ var trailers []string
233+ seen := map[string]bool{strings.ToLower(first.Email): true}
234+ for _, c := range oldest {
235+ msg.WriteString("- " + singleLine(c.Summary) + "\n")
236+ key := strings.ToLower(c.Author.Email)
237+ if !seen[key] {
238+ seen[key] = true
239+ trailers = append(trailers, "Co-authored-by: "+identLine(c.Author.Name, c.Author.Email))
240+ }
241+ }
242+ if len(trailers) > 0 {
243+ msg.WriteString("\n" + strings.Join(trailers, "\n") + "\n")
244+ }
245+ return SquashCommit{
246+ Tree: tree, Parent: parent,
247+ Author: first,
248+ Committer: Signature{Name: committer.Name, Email: committer.Email, Time: at.UTC()},
249+ Message: msg.String(),
250+ }
251+}
252+
253+// Payload returns the text of the commit object without a signature, which is
254+// what the signature covers.
255+func (c SquashCommit) Payload() []byte { return c.encode("") }
256+
257+// Signed returns the text of the commit object with the armored SSH signature
258+// in its gpgsig header.
259+func (c SquashCommit) Signed(armored string) []byte { return c.encode(armored) }
260+
261+func (c SquashCommit) encode(armored string) []byte {
262+ var b strings.Builder
263+ b.WriteString("tree " + c.Tree.String() + "\n")
264+ b.WriteString("parent " + c.Parent.String() + "\n")
265+ b.WriteString("author " + signatureLine(c.Author) + "\n")
266+ b.WriteString("committer " + signatureLine(c.Committer) + "\n")
267+ if armored != "" {
268+ lines := strings.Split(strings.TrimRight(armored, "\n"), "\n")
269+ b.WriteString("gpgsig " + lines[0] + "\n")
270+ for _, l := range lines[1:] {
271+ b.WriteString(" " + l + "\n")
272+ }
273+ }
274+ b.WriteString("\n")
275+ b.WriteString(c.Message)
276+ return []byte(b.String())
277+}
278+
279+func signatureLine(s Signature) string {
280+ return fmt.Sprintf("%s %d +0000", identLine(s.Name, s.Email), s.Time.Unix())
281+}
282+
283+// identLine formats "Name <email>", dropping the characters Git does not
284+// allow in an identity.
285+func identLine(name, email string) string {
286+ clean := func(s string) string {
287+ return strings.TrimSpace(strings.Map(func(r rune) rune {
288+ if r == '<' || r == '>' || r < 0x20 || r == 0x7f {
289+ return -1
290+ }
291+ return r
292+ }, s))
293+ }
294+ return clean(name) + " <" + clean(email) + ">"
295+}
296+
297+// singleLine replaces the control characters of a commit summary, so that it
298+// stays one line of the message.
299+func singleLine(s string) string {
300+ return strings.Map(func(r rune) rune {
301+ if r < 0x20 || r == 0x7f {
302+ return ' '
303+ }
304+ return r
305+ }, s)
306+}
A internal/domain/mergerequest_test.go +203 -0
@@ -0,0 +1,203 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain_test
4+
5+import (
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+
13+ "bnema.dev/ermite/internal/domain"
14+)
15+
16+func oid(t *testing.T, c byte) domain.ObjectID {
17+ t.Helper()
18+ id, err := domain.ParseObjectID(strings.Repeat(string(c), domain.ObjectIDLen))
19+ require.NoError(t, err)
20+ return id
21+}
22+
23+func TestDefaultRepoSettings(t *testing.T) {
24+ s := domain.DefaultRepoSettings()
25+ assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}, s.AllowedMergeModes)
26+ assert.True(t, s.FailingChecksBlock)
27+ assert.True(t, s.Allows(domain.MergeModeSquash))
28+ assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash))
29+ assert.True(t, domain.RepoSettings{}.IsZero())
30+ assert.False(t, s.IsZero())
31+
32+ // The default list is a copy: callers cannot alter the next default.
33+ s.AllowedMergeModes[0] = domain.MergeModeSquash
34+ assert.Equal(t, domain.MergeModeFastForward, domain.DefaultRepoSettings().AllowedMergeModes[0])
35+}
36+
37+func TestNormalizeMergeModes(t *testing.T) {
38+ got, err := domain.NormalizeMergeModes([]domain.MergeMode{domain.MergeModeSquash, domain.MergeModeFastForward, domain.MergeModeSquash})
39+ require.NoError(t, err)
40+ assert.Equal(t, domain.MergeModes(), got)
41+
42+ got, err = domain.NormalizeMergeModes([]domain.MergeMode{domain.MergeModeSquash})
43+ require.NoError(t, err)
44+ assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, got)
45+
46+ _, err = domain.NormalizeMergeModes(nil)
47+ assert.ErrorIs(t, err, domain.ErrInvalid)
48+ _, err = domain.NormalizeMergeModes([]domain.MergeMode{"rebase"})
49+ assert.ErrorIs(t, err, domain.ErrInvalid)
50+}
51+
52+func TestParseMergeMode(t *testing.T) {
53+ m, err := domain.ParseMergeMode("fast-forward")
54+ require.NoError(t, err)
55+ assert.Equal(t, domain.MergeModeFastForward, m)
56+ _, err = domain.ParseMergeMode("merge")
57+ assert.ErrorIs(t, err, domain.ErrInvalid)
58+}
59+
60+func hunkDiff() domain.Diff {
61+ return domain.Diff{Files: []domain.FileDiff{
62+ {OldPath: "a.txt", NewPath: "a.txt", Status: domain.FileModified, Hunks: []domain.Hunk{{
63+ Lines: []domain.Line{
64+ {Kind: domain.LineContext, OldNumber: 4, NewNumber: 4},
65+ {Kind: domain.LineDeletion, OldNumber: 5},
66+ {Kind: domain.LineAddition, NewNumber: 5},
67+ {Kind: domain.LineAddition, NewNumber: 6},
68+ },
69+ }}},
70+ {OldPath: "gone.txt", Status: domain.FileDeleted, Hunks: []domain.Hunk{{
71+ Lines: []domain.Line{{Kind: domain.LineDeletion, OldNumber: 1}},
72+ }}},
73+ {NewPath: "big.txt", Status: domain.FileAdded, Truncated: true},
74+ {OldPath: "old.txt", NewPath: "new.txt", Status: domain.FileRenamed, Hunks: []domain.Hunk{{
75+ Lines: []domain.Line{{Kind: domain.LineAddition, NewNumber: 2}},
76+ }}},
77+ }}
78+}
79+
80+func TestDiffHasLine(t *testing.T) {
81+ d := hunkDiff()
82+ tests := []struct {
83+ name string
84+ path string
85+ side domain.DiffSide
86+ line int
87+ want bool
88+ }{
89+ {"context on the new side", "a.txt", domain.SideNew, 4, true},
90+ {"context on the old side", "a.txt", domain.SideOld, 4, true},
91+ {"deletion on the old side", "a.txt", domain.SideOld, 5, true},
92+ {"addition on the new side", "a.txt", domain.SideNew, 6, true},
93+ {"addition is not on the old side", "a.txt", domain.SideOld, 6, false},
94+ {"new line 5 is the addition, not the deletion", "a.txt", domain.SideNew, 5, true},
95+ {"line outside the hunk", "a.txt", domain.SideNew, 99, false},
96+ {"line zero", "a.txt", domain.SideNew, 0, false},
97+ {"negative line", "a.txt", domain.SideNew, -1, false},
98+ {"no side", "a.txt", domain.SideNone, 4, false},
99+ {"unknown path", "nope.txt", domain.SideNew, 4, false},
100+ {"empty path", "", domain.SideNew, 4, false},
101+ {"deleted file by its old path", "gone.txt", domain.SideOld, 1, true},
102+ {"deleted file has no new side", "gone.txt", domain.SideNew, 1, false},
103+ {"truncated file has no lines", "big.txt", domain.SideNew, 1, false},
104+ {"renamed file by its new path", "new.txt", domain.SideNew, 2, true},
105+ {"renamed file by its old path", "old.txt", domain.SideNew, 2, false},
106+ }
107+ for _, tc := range tests {
108+ t.Run(tc.name, func(t *testing.T) {
109+ assert.Equal(t, tc.want, d.HasLine(tc.path, tc.side, tc.line))
110+ })
111+ }
112+}
113+
114+func sig(name, email string, sec int64) domain.Signature {
115+ return domain.Signature{Name: name, Email: email, Time: time.Unix(sec, 0).UTC()}
116+}
117+
118+func TestNewSquashCommit(t *testing.T) {
119+ title, err := domain.ParseMRTitle("Add search")
120+ require.NoError(t, err)
121+ mr := domain.MergeRequest{Number: 7, Title: title}
122+ // Newest first, as Git lists them.
123+ commits := []domain.Commit{
124+ {Summary: "fix typo", Author: sig("Bob", "bob@example.org", 300)},
125+ {Summary: "add tests", Author: sig("Ada", "ADA@example.org", 200)},
126+ {Summary: "add search", Author: sig("Ada", "ada@example.org", 100)},
127+ {Summary: "wip", Author: sig("Cy", "cy@example.org", 150)},
128+ {Summary: "polish", Author: sig("Bob", "bob@example.org", 160)},
129+ }
130+ committer := domain.Identity{Name: "ermite", Email: "ermite@git.example.org"}
131+ at := time.Unix(1_800_000_000, 0)
132+
133+ c := domain.NewSquashCommit(mr, oid(t, 'a'), oid(t, 'b'), commits, committer, at)
134+
135+ // The author is the author of the oldest commit (the last one listed).
136+ assert.Equal(t, sig("Bob", "bob@example.org", 160), c.Author)
137+ assert.Equal(t, domain.Signature{Name: "ermite", Email: "ermite@git.example.org", Time: at.UTC()}, c.Committer)
138+ assert.Equal(t, "Add search (!7)\n"+
139+ "\n"+
140+ "- polish\n- wip\n- add search\n- add tests\n- fix typo\n"+
141+ "\n"+
142+ "Co-authored-by: Cy <cy@example.org>\n"+
143+ "Co-authored-by: Ada <ada@example.org>\n", c.Message)
144+
145+ want := "tree " + strings.Repeat("a", 40) + "\n" +
146+ "parent " + strings.Repeat("b", 40) + "\n" +
147+ "author Bob <bob@example.org> 160 +0000\n" +
148+ "committer ermite <ermite@git.example.org> 1800000000 +0000\n" +
149+ "\n" + c.Message
150+ assert.Equal(t, want, string(c.Payload()))
151+}
152+
153+func TestSquashCommitSigned(t *testing.T) {
154+ title, err := domain.ParseMRTitle("T")
155+ require.NoError(t, err)
156+ c := domain.NewSquashCommit(domain.MergeRequest{Number: 1, Title: title}, oid(t, 'a'), oid(t, 'b'),
157+ []domain.Commit{{Summary: "s", Author: sig("A", "a@x.org", 1)}}, domain.Identity{Name: "e", Email: "e@x.org"}, time.Unix(2, 0))
158+
159+ got := string(c.Signed("-----BEGIN SSH SIGNATURE-----\nAAAA\nBBBB\n-----END SSH SIGNATURE-----\n"))
160+ assert.Equal(t, "tree "+strings.Repeat("a", 40)+"\n"+
161+ "parent "+strings.Repeat("b", 40)+"\n"+
162+ "author A <a@x.org> 1 +0000\n"+
163+ "committer e <e@x.org> 2 +0000\n"+
164+ "gpgsig -----BEGIN SSH SIGNATURE-----\n"+
165+ " AAAA\n"+
166+ " BBBB\n"+
167+ " -----END SSH SIGNATURE-----\n"+
168+ "\n"+c.Message, got)
169+}
170+
171+func TestSquashCommitCleansIdentitiesAndSummaries(t *testing.T) {
172+ title, err := domain.ParseMRTitle("T")
173+ require.NoError(t, err)
174+ commits := []domain.Commit{
175+ {Summary: "second", Author: sig("Eve <evil@x.org>\nCo-authored-by: Z", "eve@x.org", 2)},
176+ {Summary: "first\x1b[0m", Author: sig("Ada", "ada@x.org", 1)},
177+ }
178+ c := domain.NewSquashCommit(domain.MergeRequest{Number: 2, Title: title}, oid(t, 'a'), oid(t, 'b'), commits,
179+ domain.Identity{Name: "e", Email: "e@x.org"}, time.Unix(3, 0))
180+ assert.Contains(t, c.Message, "- first [0m\n")
181+ assert.Contains(t, c.Message, "\nCo-authored-by: Eve evil@x.orgCo-authored-by: Z <eve@x.org>\n")
182+ assert.NotContains(t, c.Message, "\nCo-authored-by: Z")
183+}
184+
185+func TestSigningKeyInfoAllowedSigners(t *testing.T) {
186+ i := domain.SigningKeyInfo{
187+ Key: domain.SigningPublicKey{AuthorizedKey: "ssh-ed25519 AAAA"},
188+ Committer: domain.Identity{Name: "ermite", Email: "ermite@git.example.org"},
189+ }
190+ assert.Equal(t, "ermite@git.example.org ssh-ed25519 AAAA", i.AllowedSigners())
191+}
192+
193+func TestParseBranchName(t *testing.T) {
194+ for _, ok := range []string{"main", "feat/x", "v1.2", "a-b_c"} {
195+ got, err := domain.ParseBranchName(ok)
196+ require.NoError(t, err, ok)
197+ assert.Equal(t, ok, got)
198+ }
199+ for _, bad := range []string{"", "refs/heads/main", "-x", "a..b", "a b", "x/", "a:b", "x.lock", "a\x00b"} {
200+ _, err := domain.ParseBranchName(bad)
201+ assert.ErrorIs(t, err, domain.ErrInvalidRevision, "%q", bad)
202+ }
203+}
@@ -0,0 +1,203 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain_test
4+
5+import (
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+
13+ "bnema.dev/ermite/internal/domain"
14+)
15+
16+func oid(t *testing.T, c byte) domain.ObjectID {
17+ t.Helper()
18+ id, err := domain.ParseObjectID(strings.Repeat(string(c), domain.ObjectIDLen))
19+ require.NoError(t, err)
20+ return id
21+}
22+
23+func TestDefaultRepoSettings(t *testing.T) {
24+ s := domain.DefaultRepoSettings()
25+ assert.Equal(t, []domain.MergeMode{domain.MergeModeFastForward, domain.MergeModeSquash}, s.AllowedMergeModes)
26+ assert.True(t, s.FailingChecksBlock)
27+ assert.True(t, s.Allows(domain.MergeModeSquash))
28+ assert.False(t, domain.RepoSettings{}.Allows(domain.MergeModeSquash))
29+ assert.True(t, domain.RepoSettings{}.IsZero())
30+ assert.False(t, s.IsZero())
31+
32+ // The default list is a copy: callers cannot alter the next default.
33+ s.AllowedMergeModes[0] = domain.MergeModeSquash
34+ assert.Equal(t, domain.MergeModeFastForward, domain.DefaultRepoSettings().AllowedMergeModes[0])
35+}
36+
37+func TestNormalizeMergeModes(t *testing.T) {
38+ got, err := domain.NormalizeMergeModes([]domain.MergeMode{domain.MergeModeSquash, domain.MergeModeFastForward, domain.MergeModeSquash})
39+ require.NoError(t, err)
40+ assert.Equal(t, domain.MergeModes(), got)
41+
42+ got, err = domain.NormalizeMergeModes([]domain.MergeMode{domain.MergeModeSquash})
43+ require.NoError(t, err)
44+ assert.Equal(t, []domain.MergeMode{domain.MergeModeSquash}, got)
45+
46+ _, err = domain.NormalizeMergeModes(nil)
47+ assert.ErrorIs(t, err, domain.ErrInvalid)
48+ _, err = domain.NormalizeMergeModes([]domain.MergeMode{"rebase"})
49+ assert.ErrorIs(t, err, domain.ErrInvalid)
50+}
51+
52+func TestParseMergeMode(t *testing.T) {
53+ m, err := domain.ParseMergeMode("fast-forward")
54+ require.NoError(t, err)
55+ assert.Equal(t, domain.MergeModeFastForward, m)
56+ _, err = domain.ParseMergeMode("merge")
57+ assert.ErrorIs(t, err, domain.ErrInvalid)
58+}
59+
60+func hunkDiff() domain.Diff {
61+ return domain.Diff{Files: []domain.FileDiff{
62+ {OldPath: "a.txt", NewPath: "a.txt", Status: domain.FileModified, Hunks: []domain.Hunk{{
63+ Lines: []domain.Line{
64+ {Kind: domain.LineContext, OldNumber: 4, NewNumber: 4},
65+ {Kind: domain.LineDeletion, OldNumber: 5},
66+ {Kind: domain.LineAddition, NewNumber: 5},
67+ {Kind: domain.LineAddition, NewNumber: 6},
68+ },
69+ }}},
70+ {OldPath: "gone.txt", Status: domain.FileDeleted, Hunks: []domain.Hunk{{
71+ Lines: []domain.Line{{Kind: domain.LineDeletion, OldNumber: 1}},
72+ }}},
73+ {NewPath: "big.txt", Status: domain.FileAdded, Truncated: true},
74+ {OldPath: "old.txt", NewPath: "new.txt", Status: domain.FileRenamed, Hunks: []domain.Hunk{{
75+ Lines: []domain.Line{{Kind: domain.LineAddition, NewNumber: 2}},
76+ }}},
77+ }}
78+}
79+
80+func TestDiffHasLine(t *testing.T) {
81+ d := hunkDiff()
82+ tests := []struct {
83+ name string
84+ path string
85+ side domain.DiffSide
86+ line int
87+ want bool
88+ }{
89+ {"context on the new side", "a.txt", domain.SideNew, 4, true},
90+ {"context on the old side", "a.txt", domain.SideOld, 4, true},
91+ {"deletion on the old side", "a.txt", domain.SideOld, 5, true},
92+ {"addition on the new side", "a.txt", domain.SideNew, 6, true},
93+ {"addition is not on the old side", "a.txt", domain.SideOld, 6, false},
94+ {"new line 5 is the addition, not the deletion", "a.txt", domain.SideNew, 5, true},
95+ {"line outside the hunk", "a.txt", domain.SideNew, 99, false},
96+ {"line zero", "a.txt", domain.SideNew, 0, false},
97+ {"negative line", "a.txt", domain.SideNew, -1, false},
98+ {"no side", "a.txt", domain.SideNone, 4, false},
99+ {"unknown path", "nope.txt", domain.SideNew, 4, false},
100+ {"empty path", "", domain.SideNew, 4, false},
101+ {"deleted file by its old path", "gone.txt", domain.SideOld, 1, true},
102+ {"deleted file has no new side", "gone.txt", domain.SideNew, 1, false},
103+ {"truncated file has no lines", "big.txt", domain.SideNew, 1, false},
104+ {"renamed file by its new path", "new.txt", domain.SideNew, 2, true},
105+ {"renamed file by its old path", "old.txt", domain.SideNew, 2, false},
106+ }
107+ for _, tc := range tests {
108+ t.Run(tc.name, func(t *testing.T) {
109+ assert.Equal(t, tc.want, d.HasLine(tc.path, tc.side, tc.line))
110+ })
111+ }
112+}
113+
114+func sig(name, email string, sec int64) domain.Signature {
115+ return domain.Signature{Name: name, Email: email, Time: time.Unix(sec, 0).UTC()}
116+}
117+
118+func TestNewSquashCommit(t *testing.T) {
119+ title, err := domain.ParseMRTitle("Add search")
120+ require.NoError(t, err)
121+ mr := domain.MergeRequest{Number: 7, Title: title}
122+ // Newest first, as Git lists them.
123+ commits := []domain.Commit{
124+ {Summary: "fix typo", Author: sig("Bob", "bob@example.org", 300)},
125+ {Summary: "add tests", Author: sig("Ada", "ADA@example.org", 200)},
126+ {Summary: "add search", Author: sig("Ada", "ada@example.org", 100)},
127+ {Summary: "wip", Author: sig("Cy", "cy@example.org", 150)},
128+ {Summary: "polish", Author: sig("Bob", "bob@example.org", 160)},
129+ }
130+ committer := domain.Identity{Name: "ermite", Email: "ermite@git.example.org"}
131+ at := time.Unix(1_800_000_000, 0)
132+
133+ c := domain.NewSquashCommit(mr, oid(t, 'a'), oid(t, 'b'), commits, committer, at)
134+
135+ // The author is the author of the oldest commit (the last one listed).
136+ assert.Equal(t, sig("Bob", "bob@example.org", 160), c.Author)
137+ assert.Equal(t, domain.Signature{Name: "ermite", Email: "ermite@git.example.org", Time: at.UTC()}, c.Committer)
138+ assert.Equal(t, "Add search (!7)\n"+
139+ "\n"+
140+ "- polish\n- wip\n- add search\n- add tests\n- fix typo\n"+
141+ "\n"+
142+ "Co-authored-by: Cy <cy@example.org>\n"+
143+ "Co-authored-by: Ada <ada@example.org>\n", c.Message)
144+
145+ want := "tree " + strings.Repeat("a", 40) + "\n" +
146+ "parent " + strings.Repeat("b", 40) + "\n" +
147+ "author Bob <bob@example.org> 160 +0000\n" +
148+ "committer ermite <ermite@git.example.org> 1800000000 +0000\n" +
149+ "\n" + c.Message
150+ assert.Equal(t, want, string(c.Payload()))
151+}
152+
153+func TestSquashCommitSigned(t *testing.T) {
154+ title, err := domain.ParseMRTitle("T")
155+ require.NoError(t, err)
156+ c := domain.NewSquashCommit(domain.MergeRequest{Number: 1, Title: title}, oid(t, 'a'), oid(t, 'b'),
157+ []domain.Commit{{Summary: "s", Author: sig("A", "a@x.org", 1)}}, domain.Identity{Name: "e", Email: "e@x.org"}, time.Unix(2, 0))
158+
159+ got := string(c.Signed("-----BEGIN SSH SIGNATURE-----\nAAAA\nBBBB\n-----END SSH SIGNATURE-----\n"))
160+ assert.Equal(t, "tree "+strings.Repeat("a", 40)+"\n"+
161+ "parent "+strings.Repeat("b", 40)+"\n"+
162+ "author A <a@x.org> 1 +0000\n"+
163+ "committer e <e@x.org> 2 +0000\n"+
164+ "gpgsig -----BEGIN SSH SIGNATURE-----\n"+
165+ " AAAA\n"+
166+ " BBBB\n"+
167+ " -----END SSH SIGNATURE-----\n"+
168+ "\n"+c.Message, got)
169+}
170+
171+func TestSquashCommitCleansIdentitiesAndSummaries(t *testing.T) {
172+ title, err := domain.ParseMRTitle("T")
173+ require.NoError(t, err)
174+ commits := []domain.Commit{
175+ {Summary: "second", Author: sig("Eve <evil@x.org>\nCo-authored-by: Z", "eve@x.org", 2)},
176+ {Summary: "first\x1b[0m", Author: sig("Ada", "ada@x.org", 1)},
177+ }
178+ c := domain.NewSquashCommit(domain.MergeRequest{Number: 2, Title: title}, oid(t, 'a'), oid(t, 'b'), commits,
179+ domain.Identity{Name: "e", Email: "e@x.org"}, time.Unix(3, 0))
180+ assert.Contains(t, c.Message, "- first [0m\n")
181+ assert.Contains(t, c.Message, "\nCo-authored-by: Eve evil@x.orgCo-authored-by: Z <eve@x.org>\n")
182+ assert.NotContains(t, c.Message, "\nCo-authored-by: Z")
183+}
184+
185+func TestSigningKeyInfoAllowedSigners(t *testing.T) {
186+ i := domain.SigningKeyInfo{
187+ Key: domain.SigningPublicKey{AuthorizedKey: "ssh-ed25519 AAAA"},
188+ Committer: domain.Identity{Name: "ermite", Email: "ermite@git.example.org"},
189+ }
190+ assert.Equal(t, "ermite@git.example.org ssh-ed25519 AAAA", i.AllowedSigners())
191+}
192+
193+func TestParseBranchName(t *testing.T) {
194+ for _, ok := range []string{"main", "feat/x", "v1.2", "a-b_c"} {
195+ got, err := domain.ParseBranchName(ok)
196+ require.NoError(t, err, ok)
197+ assert.Equal(t, ok, got)
198+ }
199+ for _, bad := range []string{"", "refs/heads/main", "-x", "a..b", "a b", "x/", "a:b", "x.lock", "a\x00b"} {
200+ _, err := domain.ParseBranchName(bad)
201+ assert.ErrorIs(t, err, domain.ErrInvalidRevision, "%q", bad)
202+ }
203+}
M internal/domain/principal.go +6 -0
@@ -25,6 +25,7 @@ type Principal struct {
2525 scopes []Scope
2626 repos []RepoName
2727 expiresAt time.Time
28+ label string
2829 }
2930
3031 // Anonymous returns the principal of a caller without credentials.
@@ -41,6 +42,7 @@ func TokenPrincipal(t Token) Principal {
4142 scopes: slices.Clone(t.Scopes),
4243 repos: slices.Clone(t.Repos),
4344 expiresAt: t.ExpiresAt,
45+ label: t.Label,
4446 }
4547 }
4648
@@ -98,3 +100,7 @@ func (p Principal) Allows(s Scope, repo RepoName) bool {
98100 }
99101 return true
100102 }
103+
104+// TokenLabel returns the label of the stored token, or "" for other
105+// principals.
106+func (p Principal) TokenLabel() string { return p.label }
@@ -25,6 +25,7 @@ type Principal struct {
25 scopes []Scope25 scopes []Scope
26 repos []RepoName26 repos []RepoName
27 expiresAt time.Time27 expiresAt time.Time
28+ label string
28 }29 }
29 30
30 // Anonymous returns the principal of a caller without credentials.31 // Anonymous returns the principal of a caller without credentials.
@@ -41,6 +42,7 @@ func TokenPrincipal(t Token) Principal {
41 scopes: slices.Clone(t.Scopes),42 scopes: slices.Clone(t.Scopes),
42 repos: slices.Clone(t.Repos),43 repos: slices.Clone(t.Repos),
43 expiresAt: t.ExpiresAt,44 expiresAt: t.ExpiresAt,
45+ label: t.Label,
44 }46 }
45 }47 }
46 48
@@ -98,3 +100,7 @@ func (p Principal) Allows(s Scope, repo RepoName) bool {
98 }100 }
99 return true101 return true
100 }102 }
103+
104+// TokenLabel returns the label of the stored token, or "" for other
105+// principals.
106+func (p Principal) TokenLabel() string { return p.label }
M internal/domain/repo.go +3 -0
@@ -38,4 +38,7 @@ type Repo struct {
3838 Visibility Visibility
3939 CreatedAt time.Time
4040 PushedAt time.Time // zero until the first push
41+ // Settings are the merge settings; the stores always fill them in, with
42+ // the defaults for a repository that never stored any.
43+ Settings RepoSettings
4144 }
@@ -38,4 +38,7 @@ type Repo struct {
38 Visibility Visibility38 Visibility Visibility
39 CreatedAt time.Time39 CreatedAt time.Time
40 PushedAt time.Time // zero until the first push40 PushedAt time.Time // zero until the first push
41+ // Settings are the merge settings; the stores always fill them in, with
42+ // the defaults for a repository that never stored any.
43+ Settings RepoSettings
41 }44 }
A internal/domain/signing.go +26 -0
@@ -0,0 +1,26 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+// SigningPublicKey is the public half of the key with which ermite signs the
6+// commits it creates (ADR 0022).
7+type SigningPublicKey struct {
8+ // AuthorizedKey is the key as one OpenSSH authorized_keys line, for
9+ // example "ssh-ed25519 AAAA...".
10+ AuthorizedKey string
11+ // Fingerprint is the SHA-256 fingerprint in the form ssh-keygen prints,
12+ // "SHA256:<base64>".
13+ Fingerprint string
14+}
15+
16+// SigningKeyInfo is what a verifier needs to check the commits of ermite.
17+type SigningKeyInfo struct {
18+ Key SigningPublicKey
19+ Committer Identity
20+}
21+
22+// AllowedSigners returns the line of an allowed signers file for the key:
23+// "<committer email> <authorized key>".
24+func (i SigningKeyInfo) AllowedSigners() string {
25+ return i.Committer.Email + " " + i.Key.AuthorizedKey
26+}
@@ -0,0 +1,26 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+// SigningPublicKey is the public half of the key with which ermite signs the
6+// commits it creates (ADR 0022).
7+type SigningPublicKey struct {
8+ // AuthorizedKey is the key as one OpenSSH authorized_keys line, for
9+ // example "ssh-ed25519 AAAA...".
10+ AuthorizedKey string
11+ // Fingerprint is the SHA-256 fingerprint in the form ssh-keygen prints,
12+ // "SHA256:<base64>".
13+ Fingerprint string
14+}
15+
16+// SigningKeyInfo is what a verifier needs to check the commits of ermite.
17+type SigningKeyInfo struct {
18+ Key SigningPublicKey
19+ Committer Identity
20+}
21+
22+// AllowedSigners returns the line of an allowed signers file for the key:
23+// "<committer email> <authorized key>".
24+func (i SigningKeyInfo) AllowedSigners() string {
25+ return i.Committer.Email + " " + i.Key.AuthorizedKey
26+}
A internal/domain/text.go +176 -0
@@ -0,0 +1,176 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "unicode"
9+ "unicode/utf8"
10+)
11+
12+// Limits of the plain text of a merge request (ADR 0017), in characters.
13+const (
14+ // MaxTitleLen is the longest merge request title.
15+ MaxTitleLen = 120
16+ // MaxTextLen is the longest description or comment.
17+ MaxTextLen = 8000
18+)
19+
20+// ErrInvalidText means a text breaks the rules of ParseMRTitle or
21+// ParsePlainText.
22+var ErrInvalidText = errors.New("invalid text")
23+
24+// TextError says why a text is invalid. It matches ErrInvalidText.
25+type TextError struct {
26+ // Reason completes the sentence "the text ...", for example "is not valid
27+ // UTF-8".
28+ Reason string
29+}
30+
31+// Error implements error.
32+func (e *TextError) Error() string { return "text " + e.Reason }
33+
34+// Unwrap makes errors.Is(err, ErrInvalidText) true.
35+func (e *TextError) Unwrap() error { return ErrInvalidText }
36+
37+// MRTitle is a validated merge request title: one line of plain text, at most
38+// MaxTitleLen characters. Only ParseMRTitle builds a non-zero value.
39+type MRTitle struct{ s string }
40+
41+// ParseMRTitle validates s: not blank, valid UTF-8, one line, at most
42+// MaxTitleLen characters, no control character and no bidirectional override
43+// or isolate character.
44+func ParseMRTitle(s string) (MRTitle, error) {
45+ if reason := textProblem(s, false, MaxTitleLen); reason != "" {
46+ return MRTitle{}, &TextError{Reason: reason}
47+ }
48+ if isBlank(s) {
49+ return MRTitle{}, &TextError{Reason: "is empty"}
50+ }
51+ return MRTitle{s: s}, nil
52+}
53+
54+// String returns the title.
55+func (t MRTitle) String() string { return t.s }
56+
57+// IsZero reports whether t is the zero value.
58+func (t MRTitle) IsZero() bool { return t.s == "" }
59+
60+// PlainText is a validated multi-line plain text, such as a description or a
61+// comment: valid UTF-8, at most MaxTextLen characters, line breaks allowed,
62+// no other control character, no bidirectional override or isolate character.
63+// The zero value is the empty text.
64+type PlainText struct{ s string }
65+
66+// ParsePlainText validates s. The empty text is valid: a caller that needs
67+// text checks IsBlank.
68+func ParsePlainText(s string) (PlainText, error) {
69+ if reason := textProblem(s, true, MaxTextLen); reason != "" {
70+ return PlainText{}, &TextError{Reason: reason}
71+ }
72+ return PlainText{s: s}, nil
73+}
74+
75+// String returns the text.
76+func (t PlainText) String() string { return t.s }
77+
78+// IsBlank reports whether the text is empty or only white space.
79+func (t PlainText) IsBlank() bool { return isBlank(t.s) }
80+
81+func isBlank(s string) bool {
82+ for _, r := range s {
83+ if !unicode.IsSpace(r) {
84+ return false
85+ }
86+ }
87+ return true
88+}
89+
90+// textProblem returns why s breaks the rules, or "".
91+func textProblem(s string, multiline bool, maxLen int) string {
92+ if !utf8.ValidString(s) {
93+ return "is not valid UTF-8"
94+ }
95+ if n := utf8.RuneCountInString(s); n > maxLen {
96+ return fmt.Sprintf("is longer than %d characters (%d)", maxLen, n)
97+ }
98+ for _, r := range s {
99+ switch {
100+ case r == '\n':
101+ if !multiline {
102+ return "must be one line"
103+ }
104+ case unicode.IsControl(r):
105+ return fmt.Sprintf("contains the control character U+%04X", r)
106+ case isBidiControl(r):
107+ return fmt.Sprintf("contains the bidirectional override or isolate character U+%04X", r)
108+ }
109+ }
110+ return ""
111+}
112+
113+// isBidiControl reports whether r is a bidirectional embedding, override or
114+// isolate character (U+202A to U+202E, U+2066 to U+2069), which can make text
115+// read differently from how it is stored.
116+func isBidiControl(r rune) bool {
117+ return (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069)
118+}
119+
120+// FieldViolation names a request field that breaks a rule.
121+type FieldViolation struct {
122+ // Field is the name of the field in the API contract, for example
123+ // "title".
124+ Field string
125+ // Reason says what is wrong with it.
126+ Reason string
127+}
128+
129+// InvalidFieldsError is a request rejected whole because of its fields. It
130+// matches ErrInvalid.
131+type InvalidFieldsError struct {
132+ Fields []FieldViolation
133+}
134+
135+// Error implements error.
136+func (e *InvalidFieldsError) Error() string {
137+ msg := "invalid argument"
138+ for i, f := range e.Fields {
139+ sep := "; "
140+ if i == 0 {
141+ sep = ": "
142+ }
143+ msg += sep + f.Field + " " + f.Reason
144+ }
145+ return msg
146+}
147+
148+// Unwrap makes errors.Is(err, ErrInvalid) true.
149+func (e *InvalidFieldsError) Unwrap() error { return ErrInvalid }
150+
151+// Violations collects the violations of a request.
152+type Violations struct{ list []FieldViolation }
153+
154+// Add records that field breaks a rule.
155+func (v *Violations) Add(field, reason string) {
156+ v.list = append(v.list, FieldViolation{Field: field, Reason: reason})
157+}
158+
159+// AddErr records an error of a parser for field: the reason of a *TextError,
160+// else the text of the error.
161+func (v *Violations) AddErr(field string, err error) {
162+ var te *TextError
163+ if errors.As(err, &te) {
164+ v.Add(field, te.Reason)
165+ return
166+ }
167+ v.Add(field, err.Error())
168+}
169+
170+// Err returns nil without violations, else an *InvalidFieldsError.
171+func (v *Violations) Err() error {
172+ if len(v.list) == 0 {
173+ return nil
174+ }
175+ return &InvalidFieldsError{Fields: append([]FieldViolation(nil), v.list...)}
176+}
@@ -0,0 +1,176 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "unicode"
9+ "unicode/utf8"
10+)
11+
12+// Limits of the plain text of a merge request (ADR 0017), in characters.
13+const (
14+ // MaxTitleLen is the longest merge request title.
15+ MaxTitleLen = 120
16+ // MaxTextLen is the longest description or comment.
17+ MaxTextLen = 8000
18+)
19+
20+// ErrInvalidText means a text breaks the rules of ParseMRTitle or
21+// ParsePlainText.
22+var ErrInvalidText = errors.New("invalid text")
23+
24+// TextError says why a text is invalid. It matches ErrInvalidText.
25+type TextError struct {
26+ // Reason completes the sentence "the text ...", for example "is not valid
27+ // UTF-8".
28+ Reason string
29+}
30+
31+// Error implements error.
32+func (e *TextError) Error() string { return "text " + e.Reason }
33+
34+// Unwrap makes errors.Is(err, ErrInvalidText) true.
35+func (e *TextError) Unwrap() error { return ErrInvalidText }
36+
37+// MRTitle is a validated merge request title: one line of plain text, at most
38+// MaxTitleLen characters. Only ParseMRTitle builds a non-zero value.
39+type MRTitle struct{ s string }
40+
41+// ParseMRTitle validates s: not blank, valid UTF-8, one line, at most
42+// MaxTitleLen characters, no control character and no bidirectional override
43+// or isolate character.
44+func ParseMRTitle(s string) (MRTitle, error) {
45+ if reason := textProblem(s, false, MaxTitleLen); reason != "" {
46+ return MRTitle{}, &TextError{Reason: reason}
47+ }
48+ if isBlank(s) {
49+ return MRTitle{}, &TextError{Reason: "is empty"}
50+ }
51+ return MRTitle{s: s}, nil
52+}
53+
54+// String returns the title.
55+func (t MRTitle) String() string { return t.s }
56+
57+// IsZero reports whether t is the zero value.
58+func (t MRTitle) IsZero() bool { return t.s == "" }
59+
60+// PlainText is a validated multi-line plain text, such as a description or a
61+// comment: valid UTF-8, at most MaxTextLen characters, line breaks allowed,
62+// no other control character, no bidirectional override or isolate character.
63+// The zero value is the empty text.
64+type PlainText struct{ s string }
65+
66+// ParsePlainText validates s. The empty text is valid: a caller that needs
67+// text checks IsBlank.
68+func ParsePlainText(s string) (PlainText, error) {
69+ if reason := textProblem(s, true, MaxTextLen); reason != "" {
70+ return PlainText{}, &TextError{Reason: reason}
71+ }
72+ return PlainText{s: s}, nil
73+}
74+
75+// String returns the text.
76+func (t PlainText) String() string { return t.s }
77+
78+// IsBlank reports whether the text is empty or only white space.
79+func (t PlainText) IsBlank() bool { return isBlank(t.s) }
80+
81+func isBlank(s string) bool {
82+ for _, r := range s {
83+ if !unicode.IsSpace(r) {
84+ return false
85+ }
86+ }
87+ return true
88+}
89+
90+// textProblem returns why s breaks the rules, or "".
91+func textProblem(s string, multiline bool, maxLen int) string {
92+ if !utf8.ValidString(s) {
93+ return "is not valid UTF-8"
94+ }
95+ if n := utf8.RuneCountInString(s); n > maxLen {
96+ return fmt.Sprintf("is longer than %d characters (%d)", maxLen, n)
97+ }
98+ for _, r := range s {
99+ switch {
100+ case r == '\n':
101+ if !multiline {
102+ return "must be one line"
103+ }
104+ case unicode.IsControl(r):
105+ return fmt.Sprintf("contains the control character U+%04X", r)
106+ case isBidiControl(r):
107+ return fmt.Sprintf("contains the bidirectional override or isolate character U+%04X", r)
108+ }
109+ }
110+ return ""
111+}
112+
113+// isBidiControl reports whether r is a bidirectional embedding, override or
114+// isolate character (U+202A to U+202E, U+2066 to U+2069), which can make text
115+// read differently from how it is stored.
116+func isBidiControl(r rune) bool {
117+ return (r >= 0x202a && r <= 0x202e) || (r >= 0x2066 && r <= 0x2069)
118+}
119+
120+// FieldViolation names a request field that breaks a rule.
121+type FieldViolation struct {
122+ // Field is the name of the field in the API contract, for example
123+ // "title".
124+ Field string
125+ // Reason says what is wrong with it.
126+ Reason string
127+}
128+
129+// InvalidFieldsError is a request rejected whole because of its fields. It
130+// matches ErrInvalid.
131+type InvalidFieldsError struct {
132+ Fields []FieldViolation
133+}
134+
135+// Error implements error.
136+func (e *InvalidFieldsError) Error() string {
137+ msg := "invalid argument"
138+ for i, f := range e.Fields {
139+ sep := "; "
140+ if i == 0 {
141+ sep = ": "
142+ }
143+ msg += sep + f.Field + " " + f.Reason
144+ }
145+ return msg
146+}
147+
148+// Unwrap makes errors.Is(err, ErrInvalid) true.
149+func (e *InvalidFieldsError) Unwrap() error { return ErrInvalid }
150+
151+// Violations collects the violations of a request.
152+type Violations struct{ list []FieldViolation }
153+
154+// Add records that field breaks a rule.
155+func (v *Violations) Add(field, reason string) {
156+ v.list = append(v.list, FieldViolation{Field: field, Reason: reason})
157+}
158+
159+// AddErr records an error of a parser for field: the reason of a *TextError,
160+// else the text of the error.
161+func (v *Violations) AddErr(field string, err error) {
162+ var te *TextError
163+ if errors.As(err, &te) {
164+ v.Add(field, te.Reason)
165+ return
166+ }
167+ v.Add(field, err.Error())
168+}
169+
170+// Err returns nil without violations, else an *InvalidFieldsError.
171+func (v *Violations) Err() error {
172+ if len(v.list) == 0 {
173+ return nil
174+ }
175+ return &InvalidFieldsError{Fields: append([]FieldViolation(nil), v.list...)}
176+}
A internal/domain/text_test.go +126 -0
@@ -0,0 +1,126 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain_test
4+
5+import (
6+ "errors"
7+ "strings"
8+ "testing"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+
13+ "bnema.dev/ermite/internal/domain"
14+)
15+
16+// textCases are the rules shared by every text; multi says whether line breaks
17+// are allowed.
18+func TestParseMRTitle(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ in string
22+ reason string // "" when valid
23+ }{
24+ {"plain", "Add the login page", ""},
25+ {"unicode", "Ajoute la page d’accueil — ça marche 🎉", ""},
26+ {"max length in characters", strings.Repeat("é", domain.MaxTitleLen), ""},
27+ {"too long", strings.Repeat("a", domain.MaxTitleLen+1), "longer than 120"},
28+ {"empty", "", "is empty"},
29+ {"blank", " \u00a0 ", "is empty"},
30+ {"line break", "two\nlines", "one line"},
31+ {"carriage return", "a\rb", "control character U+000D"},
32+ {"tab", "a\tb", "control character U+0009"},
33+ {"nul", "a\x00b", "control character U+0000"},
34+ {"escape", "\x1b[31mred", "control character U+001B"},
35+ {"delete", "a\x7fb", "control character U+007F"},
36+ {"c1 control", "a\u0085b", "control character U+0085"},
37+ {"invalid utf-8", "a\xffb", "not valid UTF-8"},
38+ {"bidi override", "evil \u202e txt.exe", "U+202E"},
39+ {"bidi embedding", "a\u202ab", "U+202A"},
40+ {"bidi isolate", "a\u2066b", "U+2066"},
41+ {"bidi pop isolate", "a\u2069b", "U+2069"},
42+ {"zero width joiner is plain text", "a\u200db", ""},
43+ }
44+ for _, tc := range tests {
45+ t.Run(tc.name, func(t *testing.T) {
46+ got, err := domain.ParseMRTitle(tc.in)
47+ if tc.reason == "" {
48+ require.NoError(t, err)
49+ assert.Equal(t, tc.in, got.String())
50+ assert.False(t, got.IsZero())
51+ return
52+ }
53+ require.Error(t, err)
54+ assert.ErrorIs(t, err, domain.ErrInvalidText)
55+ assert.Contains(t, err.Error(), tc.reason)
56+ assert.True(t, got.IsZero())
57+ })
58+ }
59+}
60+
61+func TestParsePlainText(t *testing.T) {
62+ tests := []struct {
63+ name string
64+ in string
65+ reason string
66+ }{
67+ {"empty is valid", "", ""},
68+ {"multi-line", "one\ntwo\n\nthree", ""},
69+ {"trailing newline", "x\n", ""},
70+ {"max length", strings.Repeat("a\n", domain.MaxTextLen/2), ""},
71+ {"max length in characters not bytes", strings.Repeat("€", domain.MaxTextLen), ""},
72+ {"too long", strings.Repeat("a", domain.MaxTextLen+1), "longer than 8000"},
73+ {"carriage return", "a\r\nb", "control character U+000D"},
74+ {"tab", "a\tb", "control character U+0009"},
75+ {"escape sequence", "a\x1b]0;title\x07", "control character U+001B"},
76+ {"bell", "a\x07", "control character U+0007"},
77+ {"invalid utf-8", "ok\n\xc3\x28", "not valid UTF-8"},
78+ {"bidi override", "x\u202ey", "U+202E"},
79+ {"bidi left-to-right isolate", "x\u2066y", "U+2066"},
80+ {"bidi first strong isolate", "x\u2068y", "U+2068"},
81+ {"left-to-right mark is plain text", "x\u200ey", ""},
82+ }
83+ for _, tc := range tests {
84+ t.Run(tc.name, func(t *testing.T) {
85+ got, err := domain.ParsePlainText(tc.in)
86+ if tc.reason == "" {
87+ require.NoError(t, err)
88+ assert.Equal(t, tc.in, got.String())
89+ return
90+ }
91+ require.Error(t, err)
92+ assert.ErrorIs(t, err, domain.ErrInvalidText)
93+ assert.Contains(t, err.Error(), tc.reason)
94+ assert.Empty(t, got.String())
95+ })
96+ }
97+}
98+
99+func TestPlainTextIsBlank(t *testing.T) {
100+ for in, blank := range map[string]bool{"": true, " \n\n ": true, "x": false, " x ": false} {
101+ got, err := domain.ParsePlainText(in)
102+ require.NoError(t, err)
103+ assert.Equal(t, blank, got.IsBlank(), "%q", in)
104+ }
105+}
106+
107+func TestViolations(t *testing.T) {
108+ var v domain.Violations
109+ require.NoError(t, v.Err())
110+
111+ _, err := domain.ParseMRTitle("a\nb")
112+ v.AddErr("title", err)
113+ v.Add("source_branch", "does not exist")
114+ v.AddErr("text", errors.New("boom"))
115+
116+ err = v.Err()
117+ require.ErrorIs(t, err, domain.ErrInvalid)
118+ var fe *domain.InvalidFieldsError
119+ require.ErrorAs(t, err, &fe)
120+ assert.Equal(t, []domain.FieldViolation{
121+ {Field: "title", Reason: "must be one line"},
122+ {Field: "source_branch", Reason: "does not exist"},
123+ {Field: "text", Reason: "boom"},
124+ }, fe.Fields)
125+ assert.Equal(t, "invalid argument: title must be one line; source_branch does not exist; text boom", err.Error())
126+}
@@ -0,0 +1,126 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain_test
4+
5+import (
6+ "errors"
7+ "strings"
8+ "testing"
9+
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+
13+ "bnema.dev/ermite/internal/domain"
14+)
15+
16+// textCases are the rules shared by every text; multi says whether line breaks
17+// are allowed.
18+func TestParseMRTitle(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ in string
22+ reason string // "" when valid
23+ }{
24+ {"plain", "Add the login page", ""},
25+ {"unicode", "Ajoute la page d’accueil — ça marche 🎉", ""},
26+ {"max length in characters", strings.Repeat("é", domain.MaxTitleLen), ""},
27+ {"too long", strings.Repeat("a", domain.MaxTitleLen+1), "longer than 120"},
28+ {"empty", "", "is empty"},
29+ {"blank", " \u00a0 ", "is empty"},
30+ {"line break", "two\nlines", "one line"},
31+ {"carriage return", "a\rb", "control character U+000D"},
32+ {"tab", "a\tb", "control character U+0009"},
33+ {"nul", "a\x00b", "control character U+0000"},
34+ {"escape", "\x1b[31mred", "control character U+001B"},
35+ {"delete", "a\x7fb", "control character U+007F"},
36+ {"c1 control", "a\u0085b", "control character U+0085"},
37+ {"invalid utf-8", "a\xffb", "not valid UTF-8"},
38+ {"bidi override", "evil \u202e txt.exe", "U+202E"},
39+ {"bidi embedding", "a\u202ab", "U+202A"},
40+ {"bidi isolate", "a\u2066b", "U+2066"},
41+ {"bidi pop isolate", "a\u2069b", "U+2069"},
42+ {"zero width joiner is plain text", "a\u200db", ""},
43+ }
44+ for _, tc := range tests {
45+ t.Run(tc.name, func(t *testing.T) {
46+ got, err := domain.ParseMRTitle(tc.in)
47+ if tc.reason == "" {
48+ require.NoError(t, err)
49+ assert.Equal(t, tc.in, got.String())
50+ assert.False(t, got.IsZero())
51+ return
52+ }
53+ require.Error(t, err)
54+ assert.ErrorIs(t, err, domain.ErrInvalidText)
55+ assert.Contains(t, err.Error(), tc.reason)
56+ assert.True(t, got.IsZero())
57+ })
58+ }
59+}
60+
61+func TestParsePlainText(t *testing.T) {
62+ tests := []struct {
63+ name string
64+ in string
65+ reason string
66+ }{
67+ {"empty is valid", "", ""},
68+ {"multi-line", "one\ntwo\n\nthree", ""},
69+ {"trailing newline", "x\n", ""},
70+ {"max length", strings.Repeat("a\n", domain.MaxTextLen/2), ""},
71+ {"max length in characters not bytes", strings.Repeat("€", domain.MaxTextLen), ""},
72+ {"too long", strings.Repeat("a", domain.MaxTextLen+1), "longer than 8000"},
73+ {"carriage return", "a\r\nb", "control character U+000D"},
74+ {"tab", "a\tb", "control character U+0009"},
75+ {"escape sequence", "a\x1b]0;title\x07", "control character U+001B"},
76+ {"bell", "a\x07", "control character U+0007"},
77+ {"invalid utf-8", "ok\n\xc3\x28", "not valid UTF-8"},
78+ {"bidi override", "x\u202ey", "U+202E"},
79+ {"bidi left-to-right isolate", "x\u2066y", "U+2066"},
80+ {"bidi first strong isolate", "x\u2068y", "U+2068"},
81+ {"left-to-right mark is plain text", "x\u200ey", ""},
82+ }
83+ for _, tc := range tests {
84+ t.Run(tc.name, func(t *testing.T) {
85+ got, err := domain.ParsePlainText(tc.in)
86+ if tc.reason == "" {
87+ require.NoError(t, err)
88+ assert.Equal(t, tc.in, got.String())
89+ return
90+ }
91+ require.Error(t, err)
92+ assert.ErrorIs(t, err, domain.ErrInvalidText)
93+ assert.Contains(t, err.Error(), tc.reason)
94+ assert.Empty(t, got.String())
95+ })
96+ }
97+}
98+
99+func TestPlainTextIsBlank(t *testing.T) {
100+ for in, blank := range map[string]bool{"": true, " \n\n ": true, "x": false, " x ": false} {
101+ got, err := domain.ParsePlainText(in)
102+ require.NoError(t, err)
103+ assert.Equal(t, blank, got.IsBlank(), "%q", in)
104+ }
105+}
106+
107+func TestViolations(t *testing.T) {
108+ var v domain.Violations
109+ require.NoError(t, v.Err())
110+
111+ _, err := domain.ParseMRTitle("a\nb")
112+ v.AddErr("title", err)
113+ v.Add("source_branch", "does not exist")
114+ v.AddErr("text", errors.New("boom"))
115+
116+ err = v.Err()
117+ require.ErrorIs(t, err, domain.ErrInvalid)
118+ var fe *domain.InvalidFieldsError
119+ require.ErrorAs(t, err, &fe)
120+ assert.Equal(t, []domain.FieldViolation{
121+ {Field: "title", Reason: "must be one line"},
122+ {Field: "source_branch", Reason: "does not exist"},
123+ {Field: "text", Reason: "boom"},
124+ }, fe.Fields)
125+ assert.Equal(t, "invalid argument: title must be one line; source_branch does not exist; text boom", err.Error())
126+}
A internal/ports/mocks/CommitSigner.go +150 -0
@@ -0,0 +1,150 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "bnema.dev/ermite/internal/domain"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewCommitSigner creates a new instance of CommitSigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewCommitSigner(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *CommitSigner {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &CommitSigner{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// CommitSigner is an autogenerated mock type for the CommitSigner type
36+type CommitSigner struct {
37+ mock.Mock
38+}
39+
40+type CommitSigner_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *CommitSigner) EXPECT() *CommitSigner_Expecter {
45+ return &CommitSigner_Expecter{mock: &_m.Mock}
46+}
47+
48+// PublicKey provides a mock function for the type CommitSigner
49+func (_mock *CommitSigner) PublicKey() domain.SigningPublicKey {
50+ ret := _mock.Called()
51+
52+ if len(ret) == 0 {
53+ panic("no return value specified for PublicKey")
54+ }
55+
56+ var r0 domain.SigningPublicKey
57+ if returnFunc, ok := ret.Get(0).(func() domain.SigningPublicKey); ok {
58+ r0 = returnFunc()
59+ } else {
60+ r0 = ret.Get(0).(domain.SigningPublicKey)
61+ }
62+ return r0
63+}
64+
65+// CommitSigner_PublicKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PublicKey'
66+type CommitSigner_PublicKey_Call struct {
67+ *mock.Call
68+}
69+
70+// PublicKey is a helper method to define mock.On call
71+func (_e *CommitSigner_Expecter) PublicKey() *CommitSigner_PublicKey_Call {
72+ return &CommitSigner_PublicKey_Call{Call: _e.mock.On("PublicKey")}
73+}
74+
75+func (_c *CommitSigner_PublicKey_Call) Run(run func()) *CommitSigner_PublicKey_Call {
76+ _c.Call.Run(func(args mock.Arguments) {
77+ run()
78+ })
79+ return _c
80+}
81+
82+func (_c *CommitSigner_PublicKey_Call) Return(signingPublicKey domain.SigningPublicKey) *CommitSigner_PublicKey_Call {
83+ _c.Call.Return(signingPublicKey)
84+ return _c
85+}
86+
87+func (_c *CommitSigner_PublicKey_Call) RunAndReturn(run func() domain.SigningPublicKey) *CommitSigner_PublicKey_Call {
88+ _c.Call.Return(run)
89+ return _c
90+}
91+
92+// Sign provides a mock function for the type CommitSigner
93+func (_mock *CommitSigner) Sign(payload []byte) (string, error) {
94+ ret := _mock.Called(payload)
95+
96+ if len(ret) == 0 {
97+ panic("no return value specified for Sign")
98+ }
99+
100+ var r0 string
101+ var r1 error
102+ if returnFunc, ok := ret.Get(0).(func([]byte) (string, error)); ok {
103+ return returnFunc(payload)
104+ }
105+ if returnFunc, ok := ret.Get(0).(func([]byte) string); ok {
106+ r0 = returnFunc(payload)
107+ } else {
108+ r0 = ret.Get(0).(string)
109+ }
110+ if returnFunc, ok := ret.Get(1).(func([]byte) error); ok {
111+ r1 = returnFunc(payload)
112+ } else {
113+ r1 = ret.Error(1)
114+ }
115+ return r0, r1
116+}
117+
118+// CommitSigner_Sign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sign'
119+type CommitSigner_Sign_Call struct {
120+ *mock.Call
121+}
122+
123+// Sign is a helper method to define mock.On call
124+// - payload []byte
125+func (_e *CommitSigner_Expecter) Sign(payload any) *CommitSigner_Sign_Call {
126+ return &CommitSigner_Sign_Call{Call: _e.mock.On("Sign", payload)}
127+}
128+
129+func (_c *CommitSigner_Sign_Call) Run(run func(payload []byte)) *CommitSigner_Sign_Call {
130+ _c.Call.Run(func(args mock.Arguments) {
131+ var arg0 []byte
132+ if args[0] != nil {
133+ arg0 = args[0].([]byte)
134+ }
135+ run(
136+ arg0,
137+ )
138+ })
139+ return _c
140+}
141+
142+func (_c *CommitSigner_Sign_Call) Return(s string, err error) *CommitSigner_Sign_Call {
143+ _c.Call.Return(s, err)
144+ return _c
145+}
146+
147+func (_c *CommitSigner_Sign_Call) RunAndReturn(run func(payload []byte) (string, error)) *CommitSigner_Sign_Call {
148+ _c.Call.Return(run)
149+ return _c
150+}
@@ -0,0 +1,150 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "bnema.dev/ermite/internal/domain"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// NewCommitSigner creates a new instance of CommitSigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
13+// The first argument is typically a *testing.T value.
14+func NewCommitSigner(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *CommitSigner {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &CommitSigner{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// CommitSigner is an autogenerated mock type for the CommitSigner type
36+type CommitSigner struct {
37+ mock.Mock
38+}
39+
40+type CommitSigner_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *CommitSigner) EXPECT() *CommitSigner_Expecter {
45+ return &CommitSigner_Expecter{mock: &_m.Mock}
46+}
47+
48+// PublicKey provides a mock function for the type CommitSigner
49+func (_mock *CommitSigner) PublicKey() domain.SigningPublicKey {
50+ ret := _mock.Called()
51+
52+ if len(ret) == 0 {
53+ panic("no return value specified for PublicKey")
54+ }
55+
56+ var r0 domain.SigningPublicKey
57+ if returnFunc, ok := ret.Get(0).(func() domain.SigningPublicKey); ok {
58+ r0 = returnFunc()
59+ } else {
60+ r0 = ret.Get(0).(domain.SigningPublicKey)
61+ }
62+ return r0
63+}
64+
65+// CommitSigner_PublicKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PublicKey'
66+type CommitSigner_PublicKey_Call struct {
67+ *mock.Call
68+}
69+
70+// PublicKey is a helper method to define mock.On call
71+func (_e *CommitSigner_Expecter) PublicKey() *CommitSigner_PublicKey_Call {
72+ return &CommitSigner_PublicKey_Call{Call: _e.mock.On("PublicKey")}
73+}
74+
75+func (_c *CommitSigner_PublicKey_Call) Run(run func()) *CommitSigner_PublicKey_Call {
76+ _c.Call.Run(func(args mock.Arguments) {
77+ run()
78+ })
79+ return _c
80+}
81+
82+func (_c *CommitSigner_PublicKey_Call) Return(signingPublicKey domain.SigningPublicKey) *CommitSigner_PublicKey_Call {
83+ _c.Call.Return(signingPublicKey)
84+ return _c
85+}
86+
87+func (_c *CommitSigner_PublicKey_Call) RunAndReturn(run func() domain.SigningPublicKey) *CommitSigner_PublicKey_Call {
88+ _c.Call.Return(run)
89+ return _c
90+}
91+
92+// Sign provides a mock function for the type CommitSigner
93+func (_mock *CommitSigner) Sign(payload []byte) (string, error) {
94+ ret := _mock.Called(payload)
95+
96+ if len(ret) == 0 {
97+ panic("no return value specified for Sign")
98+ }
99+
100+ var r0 string
101+ var r1 error
102+ if returnFunc, ok := ret.Get(0).(func([]byte) (string, error)); ok {
103+ return returnFunc(payload)
104+ }
105+ if returnFunc, ok := ret.Get(0).(func([]byte) string); ok {
106+ r0 = returnFunc(payload)
107+ } else {
108+ r0 = ret.Get(0).(string)
109+ }
110+ if returnFunc, ok := ret.Get(1).(func([]byte) error); ok {
111+ r1 = returnFunc(payload)
112+ } else {
113+ r1 = ret.Error(1)
114+ }
115+ return r0, r1
116+}
117+
118+// CommitSigner_Sign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sign'
119+type CommitSigner_Sign_Call struct {
120+ *mock.Call
121+}
122+
123+// Sign is a helper method to define mock.On call
124+// - payload []byte
125+func (_e *CommitSigner_Expecter) Sign(payload any) *CommitSigner_Sign_Call {
126+ return &CommitSigner_Sign_Call{Call: _e.mock.On("Sign", payload)}
127+}
128+
129+func (_c *CommitSigner_Sign_Call) Run(run func(payload []byte)) *CommitSigner_Sign_Call {
130+ _c.Call.Run(func(args mock.Arguments) {
131+ var arg0 []byte
132+ if args[0] != nil {
133+ arg0 = args[0].([]byte)
134+ }
135+ run(
136+ arg0,
137+ )
138+ })
139+ return _c
140+}
141+
142+func (_c *CommitSigner_Sign_Call) Return(s string, err error) *CommitSigner_Sign_Call {
143+ _c.Call.Return(s, err)
144+ return _c
145+}
146+
147+func (_c *CommitSigner_Sign_Call) RunAndReturn(run func(payload []byte) (string, error)) *CommitSigner_Sign_Call {
148+ _c.Call.Return(run)
149+ return _c
150+}
A internal/ports/mocks/MergeGit.go +749 -0
@@ -0,0 +1,749 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMergeGit creates a new instance of MergeGit. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMergeGit(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MergeGit {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MergeGit{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MergeGit is an autogenerated mock type for the MergeGit type
38+type MergeGit struct {
39+ mock.Mock
40+}
41+
42+type MergeGit_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MergeGit) EXPECT() *MergeGit_Expecter {
47+ return &MergeGit_Expecter{mock: &_m.Mock}
48+}
49+
50+// BranchHead provides a mock function for the type MergeGit
51+func (_mock *MergeGit) BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error) {
52+ ret := _mock.Called(ctx, repo, branch)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for BranchHead")
56+ }
57+
58+ var r0 domain.ObjectID
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) (domain.ObjectID, error)); ok {
61+ return returnFunc(ctx, repo, branch)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) domain.ObjectID); ok {
64+ r0 = returnFunc(ctx, repo, branch)
65+ } else {
66+ r0 = ret.Get(0).(domain.ObjectID)
67+ }
68+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string) error); ok {
69+ r1 = returnFunc(ctx, repo, branch)
70+ } else {
71+ r1 = ret.Error(1)
72+ }
73+ return r0, r1
74+}
75+
76+// MergeGit_BranchHead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BranchHead'
77+type MergeGit_BranchHead_Call struct {
78+ *mock.Call
79+}
80+
81+// BranchHead is a helper method to define mock.On call
82+// - ctx context.Context
83+// - repo domain.RepoName
84+// - branch string
85+func (_e *MergeGit_Expecter) BranchHead(ctx any, repo any, branch any) *MergeGit_BranchHead_Call {
86+ return &MergeGit_BranchHead_Call{Call: _e.mock.On("BranchHead", ctx, repo, branch)}
87+}
88+
89+func (_c *MergeGit_BranchHead_Call) Run(run func(ctx context.Context, repo domain.RepoName, branch string)) *MergeGit_BranchHead_Call {
90+ _c.Call.Run(func(args mock.Arguments) {
91+ var arg0 context.Context
92+ if args[0] != nil {
93+ arg0 = args[0].(context.Context)
94+ }
95+ var arg1 domain.RepoName
96+ if args[1] != nil {
97+ arg1 = args[1].(domain.RepoName)
98+ }
99+ var arg2 string
100+ if args[2] != nil {
101+ arg2 = args[2].(string)
102+ }
103+ run(
104+ arg0,
105+ arg1,
106+ arg2,
107+ )
108+ })
109+ return _c
110+}
111+
112+func (_c *MergeGit_BranchHead_Call) Return(objectID domain.ObjectID, err error) *MergeGit_BranchHead_Call {
113+ _c.Call.Return(objectID, err)
114+ return _c
115+}
116+
117+func (_c *MergeGit_BranchHead_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)) *MergeGit_BranchHead_Call {
118+ _c.Call.Return(run)
119+ return _c
120+}
121+
122+// CommitTree provides a mock function for the type MergeGit
123+func (_mock *MergeGit) CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error) {
124+ ret := _mock.Called(ctx, repo, id)
125+
126+ if len(ret) == 0 {
127+ panic("no return value specified for CommitTree")
128+ }
129+
130+ var r0 domain.ObjectID
131+ var r1 error
132+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) (domain.ObjectID, error)); ok {
133+ return returnFunc(ctx, repo, id)
134+ }
135+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) domain.ObjectID); ok {
136+ r0 = returnFunc(ctx, repo, id)
137+ } else {
138+ r0 = ret.Get(0).(domain.ObjectID)
139+ }
140+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID) error); ok {
141+ r1 = returnFunc(ctx, repo, id)
142+ } else {
143+ r1 = ret.Error(1)
144+ }
145+ return r0, r1
146+}
147+
148+// MergeGit_CommitTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommitTree'
149+type MergeGit_CommitTree_Call struct {
150+ *mock.Call
151+}
152+
153+// CommitTree is a helper method to define mock.On call
154+// - ctx context.Context
155+// - repo domain.RepoName
156+// - id domain.ObjectID
157+func (_e *MergeGit_Expecter) CommitTree(ctx any, repo any, id any) *MergeGit_CommitTree_Call {
158+ return &MergeGit_CommitTree_Call{Call: _e.mock.On("CommitTree", ctx, repo, id)}
159+}
160+
161+func (_c *MergeGit_CommitTree_Call) Run(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID)) *MergeGit_CommitTree_Call {
162+ _c.Call.Run(func(args mock.Arguments) {
163+ var arg0 context.Context
164+ if args[0] != nil {
165+ arg0 = args[0].(context.Context)
166+ }
167+ var arg1 domain.RepoName
168+ if args[1] != nil {
169+ arg1 = args[1].(domain.RepoName)
170+ }
171+ var arg2 domain.ObjectID
172+ if args[2] != nil {
173+ arg2 = args[2].(domain.ObjectID)
174+ }
175+ run(
176+ arg0,
177+ arg1,
178+ arg2,
179+ )
180+ })
181+ return _c
182+}
183+
184+func (_c *MergeGit_CommitTree_Call) Return(objectID domain.ObjectID, err error) *MergeGit_CommitTree_Call {
185+ _c.Call.Return(objectID, err)
186+ return _c
187+}
188+
189+func (_c *MergeGit_CommitTree_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)) *MergeGit_CommitTree_Call {
190+ _c.Call.Return(run)
191+ return _c
192+}
193+
194+// DiffCommits provides a mock function for the type MergeGit
195+func (_mock *MergeGit) DiffCommits(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error) {
196+ ret := _mock.Called(ctx, repo, base, head, limits)
197+
198+ if len(ret) == 0 {
199+ panic("no return value specified for DiffCommits")
200+ }
201+
202+ var r0 domain.DiffStats
203+ var r1 domain.Diff
204+ var r2 error
205+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) (domain.DiffStats, domain.Diff, error)); ok {
206+ return returnFunc(ctx, repo, base, head, limits)
207+ }
208+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) domain.DiffStats); ok {
209+ r0 = returnFunc(ctx, repo, base, head, limits)
210+ } else {
211+ r0 = ret.Get(0).(domain.DiffStats)
212+ }
213+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) domain.Diff); ok {
214+ r1 = returnFunc(ctx, repo, base, head, limits)
215+ } else {
216+ r1 = ret.Get(1).(domain.Diff)
217+ }
218+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) error); ok {
219+ r2 = returnFunc(ctx, repo, base, head, limits)
220+ } else {
221+ r2 = ret.Error(2)
222+ }
223+ return r0, r1, r2
224+}
225+
226+// MergeGit_DiffCommits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DiffCommits'
227+type MergeGit_DiffCommits_Call struct {
228+ *mock.Call
229+}
230+
231+// DiffCommits is a helper method to define mock.On call
232+// - ctx context.Context
233+// - repo domain.RepoName
234+// - base domain.ObjectID
235+// - head domain.ObjectID
236+// - limits domain.DiffLimits
237+func (_e *MergeGit_Expecter) DiffCommits(ctx any, repo any, base any, head any, limits any) *MergeGit_DiffCommits_Call {
238+ return &MergeGit_DiffCommits_Call{Call: _e.mock.On("DiffCommits", ctx, repo, base, head, limits)}
239+}
240+
241+func (_c *MergeGit_DiffCommits_Call) Run(run func(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits)) *MergeGit_DiffCommits_Call {
242+ _c.Call.Run(func(args mock.Arguments) {
243+ var arg0 context.Context
244+ if args[0] != nil {
245+ arg0 = args[0].(context.Context)
246+ }
247+ var arg1 domain.RepoName
248+ if args[1] != nil {
249+ arg1 = args[1].(domain.RepoName)
250+ }
251+ var arg2 domain.ObjectID
252+ if args[2] != nil {
253+ arg2 = args[2].(domain.ObjectID)
254+ }
255+ var arg3 domain.ObjectID
256+ if args[3] != nil {
257+ arg3 = args[3].(domain.ObjectID)
258+ }
259+ var arg4 domain.DiffLimits
260+ if args[4] != nil {
261+ arg4 = args[4].(domain.DiffLimits)
262+ }
263+ run(
264+ arg0,
265+ arg1,
266+ arg2,
267+ arg3,
268+ arg4,
269+ )
270+ })
271+ return _c
272+}
273+
274+func (_c *MergeGit_DiffCommits_Call) Return(diffStats domain.DiffStats, diff domain.Diff, err error) *MergeGit_DiffCommits_Call {
275+ _c.Call.Return(diffStats, diff, err)
276+ return _c
277+}
278+
279+func (_c *MergeGit_DiffCommits_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error)) *MergeGit_DiffCommits_Call {
280+ _c.Call.Return(run)
281+ return _c
282+}
283+
284+// HasCommit provides a mock function for the type MergeGit
285+func (_mock *MergeGit) HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error) {
286+ ret := _mock.Called(ctx, repo, id)
287+
288+ if len(ret) == 0 {
289+ panic("no return value specified for HasCommit")
290+ }
291+
292+ var r0 bool
293+ var r1 error
294+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) (bool, error)); ok {
295+ return returnFunc(ctx, repo, id)
296+ }
297+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) bool); ok {
298+ r0 = returnFunc(ctx, repo, id)
299+ } else {
300+ r0 = ret.Get(0).(bool)
301+ }
302+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID) error); ok {
303+ r1 = returnFunc(ctx, repo, id)
304+ } else {
305+ r1 = ret.Error(1)
306+ }
307+ return r0, r1
308+}
309+
310+// MergeGit_HasCommit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasCommit'
311+type MergeGit_HasCommit_Call struct {
312+ *mock.Call
313+}
314+
315+// HasCommit is a helper method to define mock.On call
316+// - ctx context.Context
317+// - repo domain.RepoName
318+// - id domain.ObjectID
319+func (_e *MergeGit_Expecter) HasCommit(ctx any, repo any, id any) *MergeGit_HasCommit_Call {
320+ return &MergeGit_HasCommit_Call{Call: _e.mock.On("HasCommit", ctx, repo, id)}
321+}
322+
323+func (_c *MergeGit_HasCommit_Call) Run(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID)) *MergeGit_HasCommit_Call {
324+ _c.Call.Run(func(args mock.Arguments) {
325+ var arg0 context.Context
326+ if args[0] != nil {
327+ arg0 = args[0].(context.Context)
328+ }
329+ var arg1 domain.RepoName
330+ if args[1] != nil {
331+ arg1 = args[1].(domain.RepoName)
332+ }
333+ var arg2 domain.ObjectID
334+ if args[2] != nil {
335+ arg2 = args[2].(domain.ObjectID)
336+ }
337+ run(
338+ arg0,
339+ arg1,
340+ arg2,
341+ )
342+ })
343+ return _c
344+}
345+
346+func (_c *MergeGit_HasCommit_Call) Return(b bool, err error) *MergeGit_HasCommit_Call {
347+ _c.Call.Return(b, err)
348+ return _c
349+}
350+
351+func (_c *MergeGit_HasCommit_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)) *MergeGit_HasCommit_Call {
352+ _c.Call.Return(run)
353+ return _c
354+}
355+
356+// IsAncestor provides a mock function for the type MergeGit
357+func (_mock *MergeGit) IsAncestor(ctx context.Context, repo domain.RepoName, ancestor domain.ObjectID, descendant domain.ObjectID) (bool, error) {
358+ ret := _mock.Called(ctx, repo, ancestor, descendant)
359+
360+ if len(ret) == 0 {
361+ panic("no return value specified for IsAncestor")
362+ }
363+
364+ var r0 bool
365+ var r1 error
366+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) (bool, error)); ok {
367+ return returnFunc(ctx, repo, ancestor, descendant)
368+ }
369+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) bool); ok {
370+ r0 = returnFunc(ctx, repo, ancestor, descendant)
371+ } else {
372+ r0 = ret.Get(0).(bool)
373+ }
374+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) error); ok {
375+ r1 = returnFunc(ctx, repo, ancestor, descendant)
376+ } else {
377+ r1 = ret.Error(1)
378+ }
379+ return r0, r1
380+}
381+
382+// MergeGit_IsAncestor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAncestor'
383+type MergeGit_IsAncestor_Call struct {
384+ *mock.Call
385+}
386+
387+// IsAncestor is a helper method to define mock.On call
388+// - ctx context.Context
389+// - repo domain.RepoName
390+// - ancestor domain.ObjectID
391+// - descendant domain.ObjectID
392+func (_e *MergeGit_Expecter) IsAncestor(ctx any, repo any, ancestor any, descendant any) *MergeGit_IsAncestor_Call {
393+ return &MergeGit_IsAncestor_Call{Call: _e.mock.On("IsAncestor", ctx, repo, ancestor, descendant)}
394+}
395+
396+func (_c *MergeGit_IsAncestor_Call) Run(run func(ctx context.Context, repo domain.RepoName, ancestor domain.ObjectID, descendant domain.ObjectID)) *MergeGit_IsAncestor_Call {
397+ _c.Call.Run(func(args mock.Arguments) {
398+ var arg0 context.Context
399+ if args[0] != nil {
400+ arg0 = args[0].(context.Context)
401+ }
402+ var arg1 domain.RepoName
403+ if args[1] != nil {
404+ arg1 = args[1].(domain.RepoName)
405+ }
406+ var arg2 domain.ObjectID
407+ if args[2] != nil {
408+ arg2 = args[2].(domain.ObjectID)
409+ }
410+ var arg3 domain.ObjectID
411+ if args[3] != nil {
412+ arg3 = args[3].(domain.ObjectID)
413+ }
414+ run(
415+ arg0,
416+ arg1,
417+ arg2,
418+ arg3,
419+ )
420+ })
421+ return _c
422+}
423+
424+func (_c *MergeGit_IsAncestor_Call) Return(b bool, err error) *MergeGit_IsAncestor_Call {
425+ _c.Call.Return(b, err)
426+ return _c
427+}
428+
429+func (_c *MergeGit_IsAncestor_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, ancestor domain.ObjectID, descendant domain.ObjectID) (bool, error)) *MergeGit_IsAncestor_Call {
430+ _c.Call.Return(run)
431+ return _c
432+}
433+
434+// ListRange provides a mock function for the type MergeGit
435+func (_mock *MergeGit) ListRange(ctx context.Context, repo domain.RepoName, exclude domain.ObjectID, head domain.ObjectID, limit int) ([]domain.Commit, bool, error) {
436+ ret := _mock.Called(ctx, repo, exclude, head, limit)
437+
438+ if len(ret) == 0 {
439+ panic("no return value specified for ListRange")
440+ }
441+
442+ var r0 []domain.Commit
443+ var r1 bool
444+ var r2 error
445+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) ([]domain.Commit, bool, error)); ok {
446+ return returnFunc(ctx, repo, exclude, head, limit)
447+ }
448+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) []domain.Commit); ok {
449+ r0 = returnFunc(ctx, repo, exclude, head, limit)
450+ } else {
451+ if ret.Get(0) != nil {
452+ r0 = ret.Get(0).([]domain.Commit)
453+ }
454+ }
455+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) bool); ok {
456+ r1 = returnFunc(ctx, repo, exclude, head, limit)
457+ } else {
458+ r1 = ret.Get(1).(bool)
459+ }
460+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) error); ok {
461+ r2 = returnFunc(ctx, repo, exclude, head, limit)
462+ } else {
463+ r2 = ret.Error(2)
464+ }
465+ return r0, r1, r2
466+}
467+
468+// MergeGit_ListRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRange'
469+type MergeGit_ListRange_Call struct {
470+ *mock.Call
471+}
472+
473+// ListRange is a helper method to define mock.On call
474+// - ctx context.Context
475+// - repo domain.RepoName
476+// - exclude domain.ObjectID
477+// - head domain.ObjectID
478+// - limit int
479+func (_e *MergeGit_Expecter) ListRange(ctx any, repo any, exclude any, head any, limit any) *MergeGit_ListRange_Call {
480+ return &MergeGit_ListRange_Call{Call: _e.mock.On("ListRange", ctx, repo, exclude, head, limit)}
481+}
482+
483+func (_c *MergeGit_ListRange_Call) Run(run func(ctx context.Context, repo domain.RepoName, exclude domain.ObjectID, head domain.ObjectID, limit int)) *MergeGit_ListRange_Call {
484+ _c.Call.Run(func(args mock.Arguments) {
485+ var arg0 context.Context
486+ if args[0] != nil {
487+ arg0 = args[0].(context.Context)
488+ }
489+ var arg1 domain.RepoName
490+ if args[1] != nil {
491+ arg1 = args[1].(domain.RepoName)
492+ }
493+ var arg2 domain.ObjectID
494+ if args[2] != nil {
495+ arg2 = args[2].(domain.ObjectID)
496+ }
497+ var arg3 domain.ObjectID
498+ if args[3] != nil {
499+ arg3 = args[3].(domain.ObjectID)
500+ }
501+ var arg4 int
502+ if args[4] != nil {
503+ arg4 = args[4].(int)
504+ }
505+ run(
506+ arg0,
507+ arg1,
508+ arg2,
509+ arg3,
510+ arg4,
511+ )
512+ })
513+ return _c
514+}
515+
516+func (_c *MergeGit_ListRange_Call) Return(commits []domain.Commit, more bool, err error) *MergeGit_ListRange_Call {
517+ _c.Call.Return(commits, more, err)
518+ return _c
519+}
520+
521+func (_c *MergeGit_ListRange_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, exclude domain.ObjectID, head domain.ObjectID, limit int) ([]domain.Commit, bool, error)) *MergeGit_ListRange_Call {
522+ _c.Call.Return(run)
523+ return _c
524+}
525+
526+// MergeBase provides a mock function for the type MergeGit
527+func (_mock *MergeGit) MergeBase(ctx context.Context, repo domain.RepoName, a domain.ObjectID, b domain.ObjectID) (domain.ObjectID, error) {
528+ ret := _mock.Called(ctx, repo, a, b)
529+
530+ if len(ret) == 0 {
531+ panic("no return value specified for MergeBase")
532+ }
533+
534+ var r0 domain.ObjectID
535+ var r1 error
536+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) (domain.ObjectID, error)); ok {
537+ return returnFunc(ctx, repo, a, b)
538+ }
539+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) domain.ObjectID); ok {
540+ r0 = returnFunc(ctx, repo, a, b)
541+ } else {
542+ r0 = ret.Get(0).(domain.ObjectID)
543+ }
544+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) error); ok {
545+ r1 = returnFunc(ctx, repo, a, b)
546+ } else {
547+ r1 = ret.Error(1)
548+ }
549+ return r0, r1
550+}
551+
552+// MergeGit_MergeBase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MergeBase'
553+type MergeGit_MergeBase_Call struct {
554+ *mock.Call
555+}
556+
557+// MergeBase is a helper method to define mock.On call
558+// - ctx context.Context
559+// - repo domain.RepoName
560+// - a domain.ObjectID
561+// - b domain.ObjectID
562+func (_e *MergeGit_Expecter) MergeBase(ctx any, repo any, a any, b any) *MergeGit_MergeBase_Call {
563+ return &MergeGit_MergeBase_Call{Call: _e.mock.On("MergeBase", ctx, repo, a, b)}
564+}
565+
566+func (_c *MergeGit_MergeBase_Call) Run(run func(ctx context.Context, repo domain.RepoName, a domain.ObjectID, b domain.ObjectID)) *MergeGit_MergeBase_Call {
567+ _c.Call.Run(func(args mock.Arguments) {
568+ var arg0 context.Context
569+ if args[0] != nil {
570+ arg0 = args[0].(context.Context)
571+ }
572+ var arg1 domain.RepoName
573+ if args[1] != nil {
574+ arg1 = args[1].(domain.RepoName)
575+ }
576+ var arg2 domain.ObjectID
577+ if args[2] != nil {
578+ arg2 = args[2].(domain.ObjectID)
579+ }
580+ var arg3 domain.ObjectID
581+ if args[3] != nil {
582+ arg3 = args[3].(domain.ObjectID)
583+ }
584+ run(
585+ arg0,
586+ arg1,
587+ arg2,
588+ arg3,
589+ )
590+ })
591+ return _c
592+}
593+
594+func (_c *MergeGit_MergeBase_Call) Return(objectID domain.ObjectID, err error) *MergeGit_MergeBase_Call {
595+ _c.Call.Return(objectID, err)
596+ return _c
597+}
598+
599+func (_c *MergeGit_MergeBase_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, a domain.ObjectID, b domain.ObjectID) (domain.ObjectID, error)) *MergeGit_MergeBase_Call {
600+ _c.Call.Return(run)
601+ return _c
602+}
603+
604+// UpdateBranch provides a mock function for the type MergeGit
605+func (_mock *MergeGit) UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID, new domain.ObjectID) error {
606+ ret := _mock.Called(ctx, repo, branch, old, new)
607+
608+ if len(ret) == 0 {
609+ panic("no return value specified for UpdateBranch")
610+ }
611+
612+ var r0 error
613+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, domain.ObjectID, domain.ObjectID) error); ok {
614+ r0 = returnFunc(ctx, repo, branch, old, new)
615+ } else {
616+ r0 = ret.Error(0)
617+ }
618+ return r0
619+}
620+
621+// MergeGit_UpdateBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBranch'
622+type MergeGit_UpdateBranch_Call struct {
623+ *mock.Call
624+}
625+
626+// UpdateBranch is a helper method to define mock.On call
627+// - ctx context.Context
628+// - repo domain.RepoName
629+// - branch string
630+// - old domain.ObjectID
631+// - new domain.ObjectID
632+func (_e *MergeGit_Expecter) UpdateBranch(ctx any, repo any, branch any, old any, new any) *MergeGit_UpdateBranch_Call {
633+ return &MergeGit_UpdateBranch_Call{Call: _e.mock.On("UpdateBranch", ctx, repo, branch, old, new)}
634+}
635+
636+func (_c *MergeGit_UpdateBranch_Call) Run(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID, new domain.ObjectID)) *MergeGit_UpdateBranch_Call {
637+ _c.Call.Run(func(args mock.Arguments) {
638+ var arg0 context.Context
639+ if args[0] != nil {
640+ arg0 = args[0].(context.Context)
641+ }
642+ var arg1 domain.RepoName
643+ if args[1] != nil {
644+ arg1 = args[1].(domain.RepoName)
645+ }
646+ var arg2 string
647+ if args[2] != nil {
648+ arg2 = args[2].(string)
649+ }
650+ var arg3 domain.ObjectID
651+ if args[3] != nil {
652+ arg3 = args[3].(domain.ObjectID)
653+ }
654+ var arg4 domain.ObjectID
655+ if args[4] != nil {
656+ arg4 = args[4].(domain.ObjectID)
657+ }
658+ run(
659+ arg0,
660+ arg1,
661+ arg2,
662+ arg3,
663+ arg4,
664+ )
665+ })
666+ return _c
667+}
668+
669+func (_c *MergeGit_UpdateBranch_Call) Return(err error) *MergeGit_UpdateBranch_Call {
670+ _c.Call.Return(err)
671+ return _c
672+}
673+
674+func (_c *MergeGit_UpdateBranch_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID, new domain.ObjectID) error) *MergeGit_UpdateBranch_Call {
675+ _c.Call.Return(run)
676+ return _c
677+}
678+
679+// WriteCommit provides a mock function for the type MergeGit
680+func (_mock *MergeGit) WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error) {
681+ ret := _mock.Called(ctx, repo, object)
682+
683+ if len(ret) == 0 {
684+ panic("no return value specified for WriteCommit")
685+ }
686+
687+ var r0 domain.ObjectID
688+ var r1 error
689+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, []byte) (domain.ObjectID, error)); ok {
690+ return returnFunc(ctx, repo, object)
691+ }
692+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, []byte) domain.ObjectID); ok {
693+ r0 = returnFunc(ctx, repo, object)
694+ } else {
695+ r0 = ret.Get(0).(domain.ObjectID)
696+ }
697+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, []byte) error); ok {
698+ r1 = returnFunc(ctx, repo, object)
699+ } else {
700+ r1 = ret.Error(1)
701+ }
702+ return r0, r1
703+}
704+
705+// MergeGit_WriteCommit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteCommit'
706+type MergeGit_WriteCommit_Call struct {
707+ *mock.Call
708+}
709+
710+// WriteCommit is a helper method to define mock.On call
711+// - ctx context.Context
712+// - repo domain.RepoName
713+// - object []byte
714+func (_e *MergeGit_Expecter) WriteCommit(ctx any, repo any, object any) *MergeGit_WriteCommit_Call {
715+ return &MergeGit_WriteCommit_Call{Call: _e.mock.On("WriteCommit", ctx, repo, object)}
716+}
717+
718+func (_c *MergeGit_WriteCommit_Call) Run(run func(ctx context.Context, repo domain.RepoName, object []byte)) *MergeGit_WriteCommit_Call {
719+ _c.Call.Run(func(args mock.Arguments) {
720+ var arg0 context.Context
721+ if args[0] != nil {
722+ arg0 = args[0].(context.Context)
723+ }
724+ var arg1 domain.RepoName
725+ if args[1] != nil {
726+ arg1 = args[1].(domain.RepoName)
727+ }
728+ var arg2 []byte
729+ if args[2] != nil {
730+ arg2 = args[2].([]byte)
731+ }
732+ run(
733+ arg0,
734+ arg1,
735+ arg2,
736+ )
737+ })
738+ return _c
739+}
740+
741+func (_c *MergeGit_WriteCommit_Call) Return(objectID domain.ObjectID, err error) *MergeGit_WriteCommit_Call {
742+ _c.Call.Return(objectID, err)
743+ return _c
744+}
745+
746+func (_c *MergeGit_WriteCommit_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)) *MergeGit_WriteCommit_Call {
747+ _c.Call.Return(run)
748+ return _c
749+}
@@ -0,0 +1,749 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMergeGit creates a new instance of MergeGit. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMergeGit(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MergeGit {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MergeGit{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MergeGit is an autogenerated mock type for the MergeGit type
38+type MergeGit struct {
39+ mock.Mock
40+}
41+
42+type MergeGit_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MergeGit) EXPECT() *MergeGit_Expecter {
47+ return &MergeGit_Expecter{mock: &_m.Mock}
48+}
49+
50+// BranchHead provides a mock function for the type MergeGit
51+func (_mock *MergeGit) BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error) {
52+ ret := _mock.Called(ctx, repo, branch)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for BranchHead")
56+ }
57+
58+ var r0 domain.ObjectID
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) (domain.ObjectID, error)); ok {
61+ return returnFunc(ctx, repo, branch)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string) domain.ObjectID); ok {
64+ r0 = returnFunc(ctx, repo, branch)
65+ } else {
66+ r0 = ret.Get(0).(domain.ObjectID)
67+ }
68+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string) error); ok {
69+ r1 = returnFunc(ctx, repo, branch)
70+ } else {
71+ r1 = ret.Error(1)
72+ }
73+ return r0, r1
74+}
75+
76+// MergeGit_BranchHead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BranchHead'
77+type MergeGit_BranchHead_Call struct {
78+ *mock.Call
79+}
80+
81+// BranchHead is a helper method to define mock.On call
82+// - ctx context.Context
83+// - repo domain.RepoName
84+// - branch string
85+func (_e *MergeGit_Expecter) BranchHead(ctx any, repo any, branch any) *MergeGit_BranchHead_Call {
86+ return &MergeGit_BranchHead_Call{Call: _e.mock.On("BranchHead", ctx, repo, branch)}
87+}
88+
89+func (_c *MergeGit_BranchHead_Call) Run(run func(ctx context.Context, repo domain.RepoName, branch string)) *MergeGit_BranchHead_Call {
90+ _c.Call.Run(func(args mock.Arguments) {
91+ var arg0 context.Context
92+ if args[0] != nil {
93+ arg0 = args[0].(context.Context)
94+ }
95+ var arg1 domain.RepoName
96+ if args[1] != nil {
97+ arg1 = args[1].(domain.RepoName)
98+ }
99+ var arg2 string
100+ if args[2] != nil {
101+ arg2 = args[2].(string)
102+ }
103+ run(
104+ arg0,
105+ arg1,
106+ arg2,
107+ )
108+ })
109+ return _c
110+}
111+
112+func (_c *MergeGit_BranchHead_Call) Return(objectID domain.ObjectID, err error) *MergeGit_BranchHead_Call {
113+ _c.Call.Return(objectID, err)
114+ return _c
115+}
116+
117+func (_c *MergeGit_BranchHead_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)) *MergeGit_BranchHead_Call {
118+ _c.Call.Return(run)
119+ return _c
120+}
121+
122+// CommitTree provides a mock function for the type MergeGit
123+func (_mock *MergeGit) CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error) {
124+ ret := _mock.Called(ctx, repo, id)
125+
126+ if len(ret) == 0 {
127+ panic("no return value specified for CommitTree")
128+ }
129+
130+ var r0 domain.ObjectID
131+ var r1 error
132+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) (domain.ObjectID, error)); ok {
133+ return returnFunc(ctx, repo, id)
134+ }
135+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) domain.ObjectID); ok {
136+ r0 = returnFunc(ctx, repo, id)
137+ } else {
138+ r0 = ret.Get(0).(domain.ObjectID)
139+ }
140+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID) error); ok {
141+ r1 = returnFunc(ctx, repo, id)
142+ } else {
143+ r1 = ret.Error(1)
144+ }
145+ return r0, r1
146+}
147+
148+// MergeGit_CommitTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CommitTree'
149+type MergeGit_CommitTree_Call struct {
150+ *mock.Call
151+}
152+
153+// CommitTree is a helper method to define mock.On call
154+// - ctx context.Context
155+// - repo domain.RepoName
156+// - id domain.ObjectID
157+func (_e *MergeGit_Expecter) CommitTree(ctx any, repo any, id any) *MergeGit_CommitTree_Call {
158+ return &MergeGit_CommitTree_Call{Call: _e.mock.On("CommitTree", ctx, repo, id)}
159+}
160+
161+func (_c *MergeGit_CommitTree_Call) Run(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID)) *MergeGit_CommitTree_Call {
162+ _c.Call.Run(func(args mock.Arguments) {
163+ var arg0 context.Context
164+ if args[0] != nil {
165+ arg0 = args[0].(context.Context)
166+ }
167+ var arg1 domain.RepoName
168+ if args[1] != nil {
169+ arg1 = args[1].(domain.RepoName)
170+ }
171+ var arg2 domain.ObjectID
172+ if args[2] != nil {
173+ arg2 = args[2].(domain.ObjectID)
174+ }
175+ run(
176+ arg0,
177+ arg1,
178+ arg2,
179+ )
180+ })
181+ return _c
182+}
183+
184+func (_c *MergeGit_CommitTree_Call) Return(objectID domain.ObjectID, err error) *MergeGit_CommitTree_Call {
185+ _c.Call.Return(objectID, err)
186+ return _c
187+}
188+
189+func (_c *MergeGit_CommitTree_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)) *MergeGit_CommitTree_Call {
190+ _c.Call.Return(run)
191+ return _c
192+}
193+
194+// DiffCommits provides a mock function for the type MergeGit
195+func (_mock *MergeGit) DiffCommits(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error) {
196+ ret := _mock.Called(ctx, repo, base, head, limits)
197+
198+ if len(ret) == 0 {
199+ panic("no return value specified for DiffCommits")
200+ }
201+
202+ var r0 domain.DiffStats
203+ var r1 domain.Diff
204+ var r2 error
205+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) (domain.DiffStats, domain.Diff, error)); ok {
206+ return returnFunc(ctx, repo, base, head, limits)
207+ }
208+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) domain.DiffStats); ok {
209+ r0 = returnFunc(ctx, repo, base, head, limits)
210+ } else {
211+ r0 = ret.Get(0).(domain.DiffStats)
212+ }
213+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) domain.Diff); ok {
214+ r1 = returnFunc(ctx, repo, base, head, limits)
215+ } else {
216+ r1 = ret.Get(1).(domain.Diff)
217+ }
218+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, domain.DiffLimits) error); ok {
219+ r2 = returnFunc(ctx, repo, base, head, limits)
220+ } else {
221+ r2 = ret.Error(2)
222+ }
223+ return r0, r1, r2
224+}
225+
226+// MergeGit_DiffCommits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DiffCommits'
227+type MergeGit_DiffCommits_Call struct {
228+ *mock.Call
229+}
230+
231+// DiffCommits is a helper method to define mock.On call
232+// - ctx context.Context
233+// - repo domain.RepoName
234+// - base domain.ObjectID
235+// - head domain.ObjectID
236+// - limits domain.DiffLimits
237+func (_e *MergeGit_Expecter) DiffCommits(ctx any, repo any, base any, head any, limits any) *MergeGit_DiffCommits_Call {
238+ return &MergeGit_DiffCommits_Call{Call: _e.mock.On("DiffCommits", ctx, repo, base, head, limits)}
239+}
240+
241+func (_c *MergeGit_DiffCommits_Call) Run(run func(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits)) *MergeGit_DiffCommits_Call {
242+ _c.Call.Run(func(args mock.Arguments) {
243+ var arg0 context.Context
244+ if args[0] != nil {
245+ arg0 = args[0].(context.Context)
246+ }
247+ var arg1 domain.RepoName
248+ if args[1] != nil {
249+ arg1 = args[1].(domain.RepoName)
250+ }
251+ var arg2 domain.ObjectID
252+ if args[2] != nil {
253+ arg2 = args[2].(domain.ObjectID)
254+ }
255+ var arg3 domain.ObjectID
256+ if args[3] != nil {
257+ arg3 = args[3].(domain.ObjectID)
258+ }
259+ var arg4 domain.DiffLimits
260+ if args[4] != nil {
261+ arg4 = args[4].(domain.DiffLimits)
262+ }
263+ run(
264+ arg0,
265+ arg1,
266+ arg2,
267+ arg3,
268+ arg4,
269+ )
270+ })
271+ return _c
272+}
273+
274+func (_c *MergeGit_DiffCommits_Call) Return(diffStats domain.DiffStats, diff domain.Diff, err error) *MergeGit_DiffCommits_Call {
275+ _c.Call.Return(diffStats, diff, err)
276+ return _c
277+}
278+
279+func (_c *MergeGit_DiffCommits_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error)) *MergeGit_DiffCommits_Call {
280+ _c.Call.Return(run)
281+ return _c
282+}
283+
284+// HasCommit provides a mock function for the type MergeGit
285+func (_mock *MergeGit) HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error) {
286+ ret := _mock.Called(ctx, repo, id)
287+
288+ if len(ret) == 0 {
289+ panic("no return value specified for HasCommit")
290+ }
291+
292+ var r0 bool
293+ var r1 error
294+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) (bool, error)); ok {
295+ return returnFunc(ctx, repo, id)
296+ }
297+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID) bool); ok {
298+ r0 = returnFunc(ctx, repo, id)
299+ } else {
300+ r0 = ret.Get(0).(bool)
301+ }
302+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID) error); ok {
303+ r1 = returnFunc(ctx, repo, id)
304+ } else {
305+ r1 = ret.Error(1)
306+ }
307+ return r0, r1
308+}
309+
310+// MergeGit_HasCommit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasCommit'
311+type MergeGit_HasCommit_Call struct {
312+ *mock.Call
313+}
314+
315+// HasCommit is a helper method to define mock.On call
316+// - ctx context.Context
317+// - repo domain.RepoName
318+// - id domain.ObjectID
319+func (_e *MergeGit_Expecter) HasCommit(ctx any, repo any, id any) *MergeGit_HasCommit_Call {
320+ return &MergeGit_HasCommit_Call{Call: _e.mock.On("HasCommit", ctx, repo, id)}
321+}
322+
323+func (_c *MergeGit_HasCommit_Call) Run(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID)) *MergeGit_HasCommit_Call {
324+ _c.Call.Run(func(args mock.Arguments) {
325+ var arg0 context.Context
326+ if args[0] != nil {
327+ arg0 = args[0].(context.Context)
328+ }
329+ var arg1 domain.RepoName
330+ if args[1] != nil {
331+ arg1 = args[1].(domain.RepoName)
332+ }
333+ var arg2 domain.ObjectID
334+ if args[2] != nil {
335+ arg2 = args[2].(domain.ObjectID)
336+ }
337+ run(
338+ arg0,
339+ arg1,
340+ arg2,
341+ )
342+ })
343+ return _c
344+}
345+
346+func (_c *MergeGit_HasCommit_Call) Return(b bool, err error) *MergeGit_HasCommit_Call {
347+ _c.Call.Return(b, err)
348+ return _c
349+}
350+
351+func (_c *MergeGit_HasCommit_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)) *MergeGit_HasCommit_Call {
352+ _c.Call.Return(run)
353+ return _c
354+}
355+
356+// IsAncestor provides a mock function for the type MergeGit
357+func (_mock *MergeGit) IsAncestor(ctx context.Context, repo domain.RepoName, ancestor domain.ObjectID, descendant domain.ObjectID) (bool, error) {
358+ ret := _mock.Called(ctx, repo, ancestor, descendant)
359+
360+ if len(ret) == 0 {
361+ panic("no return value specified for IsAncestor")
362+ }
363+
364+ var r0 bool
365+ var r1 error
366+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) (bool, error)); ok {
367+ return returnFunc(ctx, repo, ancestor, descendant)
368+ }
369+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) bool); ok {
370+ r0 = returnFunc(ctx, repo, ancestor, descendant)
371+ } else {
372+ r0 = ret.Get(0).(bool)
373+ }
374+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) error); ok {
375+ r1 = returnFunc(ctx, repo, ancestor, descendant)
376+ } else {
377+ r1 = ret.Error(1)
378+ }
379+ return r0, r1
380+}
381+
382+// MergeGit_IsAncestor_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAncestor'
383+type MergeGit_IsAncestor_Call struct {
384+ *mock.Call
385+}
386+
387+// IsAncestor is a helper method to define mock.On call
388+// - ctx context.Context
389+// - repo domain.RepoName
390+// - ancestor domain.ObjectID
391+// - descendant domain.ObjectID
392+func (_e *MergeGit_Expecter) IsAncestor(ctx any, repo any, ancestor any, descendant any) *MergeGit_IsAncestor_Call {
393+ return &MergeGit_IsAncestor_Call{Call: _e.mock.On("IsAncestor", ctx, repo, ancestor, descendant)}
394+}
395+
396+func (_c *MergeGit_IsAncestor_Call) Run(run func(ctx context.Context, repo domain.RepoName, ancestor domain.ObjectID, descendant domain.ObjectID)) *MergeGit_IsAncestor_Call {
397+ _c.Call.Run(func(args mock.Arguments) {
398+ var arg0 context.Context
399+ if args[0] != nil {
400+ arg0 = args[0].(context.Context)
401+ }
402+ var arg1 domain.RepoName
403+ if args[1] != nil {
404+ arg1 = args[1].(domain.RepoName)
405+ }
406+ var arg2 domain.ObjectID
407+ if args[2] != nil {
408+ arg2 = args[2].(domain.ObjectID)
409+ }
410+ var arg3 domain.ObjectID
411+ if args[3] != nil {
412+ arg3 = args[3].(domain.ObjectID)
413+ }
414+ run(
415+ arg0,
416+ arg1,
417+ arg2,
418+ arg3,
419+ )
420+ })
421+ return _c
422+}
423+
424+func (_c *MergeGit_IsAncestor_Call) Return(b bool, err error) *MergeGit_IsAncestor_Call {
425+ _c.Call.Return(b, err)
426+ return _c
427+}
428+
429+func (_c *MergeGit_IsAncestor_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, ancestor domain.ObjectID, descendant domain.ObjectID) (bool, error)) *MergeGit_IsAncestor_Call {
430+ _c.Call.Return(run)
431+ return _c
432+}
433+
434+// ListRange provides a mock function for the type MergeGit
435+func (_mock *MergeGit) ListRange(ctx context.Context, repo domain.RepoName, exclude domain.ObjectID, head domain.ObjectID, limit int) ([]domain.Commit, bool, error) {
436+ ret := _mock.Called(ctx, repo, exclude, head, limit)
437+
438+ if len(ret) == 0 {
439+ panic("no return value specified for ListRange")
440+ }
441+
442+ var r0 []domain.Commit
443+ var r1 bool
444+ var r2 error
445+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) ([]domain.Commit, bool, error)); ok {
446+ return returnFunc(ctx, repo, exclude, head, limit)
447+ }
448+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) []domain.Commit); ok {
449+ r0 = returnFunc(ctx, repo, exclude, head, limit)
450+ } else {
451+ if ret.Get(0) != nil {
452+ r0 = ret.Get(0).([]domain.Commit)
453+ }
454+ }
455+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) bool); ok {
456+ r1 = returnFunc(ctx, repo, exclude, head, limit)
457+ } else {
458+ r1 = ret.Get(1).(bool)
459+ }
460+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) error); ok {
461+ r2 = returnFunc(ctx, repo, exclude, head, limit)
462+ } else {
463+ r2 = ret.Error(2)
464+ }
465+ return r0, r1, r2
466+}
467+
468+// MergeGit_ListRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRange'
469+type MergeGit_ListRange_Call struct {
470+ *mock.Call
471+}
472+
473+// ListRange is a helper method to define mock.On call
474+// - ctx context.Context
475+// - repo domain.RepoName
476+// - exclude domain.ObjectID
477+// - head domain.ObjectID
478+// - limit int
479+func (_e *MergeGit_Expecter) ListRange(ctx any, repo any, exclude any, head any, limit any) *MergeGit_ListRange_Call {
480+ return &MergeGit_ListRange_Call{Call: _e.mock.On("ListRange", ctx, repo, exclude, head, limit)}
481+}
482+
483+func (_c *MergeGit_ListRange_Call) Run(run func(ctx context.Context, repo domain.RepoName, exclude domain.ObjectID, head domain.ObjectID, limit int)) *MergeGit_ListRange_Call {
484+ _c.Call.Run(func(args mock.Arguments) {
485+ var arg0 context.Context
486+ if args[0] != nil {
487+ arg0 = args[0].(context.Context)
488+ }
489+ var arg1 domain.RepoName
490+ if args[1] != nil {
491+ arg1 = args[1].(domain.RepoName)
492+ }
493+ var arg2 domain.ObjectID
494+ if args[2] != nil {
495+ arg2 = args[2].(domain.ObjectID)
496+ }
497+ var arg3 domain.ObjectID
498+ if args[3] != nil {
499+ arg3 = args[3].(domain.ObjectID)
500+ }
501+ var arg4 int
502+ if args[4] != nil {
503+ arg4 = args[4].(int)
504+ }
505+ run(
506+ arg0,
507+ arg1,
508+ arg2,
509+ arg3,
510+ arg4,
511+ )
512+ })
513+ return _c
514+}
515+
516+func (_c *MergeGit_ListRange_Call) Return(commits []domain.Commit, more bool, err error) *MergeGit_ListRange_Call {
517+ _c.Call.Return(commits, more, err)
518+ return _c
519+}
520+
521+func (_c *MergeGit_ListRange_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, exclude domain.ObjectID, head domain.ObjectID, limit int) ([]domain.Commit, bool, error)) *MergeGit_ListRange_Call {
522+ _c.Call.Return(run)
523+ return _c
524+}
525+
526+// MergeBase provides a mock function for the type MergeGit
527+func (_mock *MergeGit) MergeBase(ctx context.Context, repo domain.RepoName, a domain.ObjectID, b domain.ObjectID) (domain.ObjectID, error) {
528+ ret := _mock.Called(ctx, repo, a, b)
529+
530+ if len(ret) == 0 {
531+ panic("no return value specified for MergeBase")
532+ }
533+
534+ var r0 domain.ObjectID
535+ var r1 error
536+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) (domain.ObjectID, error)); ok {
537+ return returnFunc(ctx, repo, a, b)
538+ }
539+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) domain.ObjectID); ok {
540+ r0 = returnFunc(ctx, repo, a, b)
541+ } else {
542+ r0 = ret.Get(0).(domain.ObjectID)
543+ }
544+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID) error); ok {
545+ r1 = returnFunc(ctx, repo, a, b)
546+ } else {
547+ r1 = ret.Error(1)
548+ }
549+ return r0, r1
550+}
551+
552+// MergeGit_MergeBase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MergeBase'
553+type MergeGit_MergeBase_Call struct {
554+ *mock.Call
555+}
556+
557+// MergeBase is a helper method to define mock.On call
558+// - ctx context.Context
559+// - repo domain.RepoName
560+// - a domain.ObjectID
561+// - b domain.ObjectID
562+func (_e *MergeGit_Expecter) MergeBase(ctx any, repo any, a any, b any) *MergeGit_MergeBase_Call {
563+ return &MergeGit_MergeBase_Call{Call: _e.mock.On("MergeBase", ctx, repo, a, b)}
564+}
565+
566+func (_c *MergeGit_MergeBase_Call) Run(run func(ctx context.Context, repo domain.RepoName, a domain.ObjectID, b domain.ObjectID)) *MergeGit_MergeBase_Call {
567+ _c.Call.Run(func(args mock.Arguments) {
568+ var arg0 context.Context
569+ if args[0] != nil {
570+ arg0 = args[0].(context.Context)
571+ }
572+ var arg1 domain.RepoName
573+ if args[1] != nil {
574+ arg1 = args[1].(domain.RepoName)
575+ }
576+ var arg2 domain.ObjectID
577+ if args[2] != nil {
578+ arg2 = args[2].(domain.ObjectID)
579+ }
580+ var arg3 domain.ObjectID
581+ if args[3] != nil {
582+ arg3 = args[3].(domain.ObjectID)
583+ }
584+ run(
585+ arg0,
586+ arg1,
587+ arg2,
588+ arg3,
589+ )
590+ })
591+ return _c
592+}
593+
594+func (_c *MergeGit_MergeBase_Call) Return(objectID domain.ObjectID, err error) *MergeGit_MergeBase_Call {
595+ _c.Call.Return(objectID, err)
596+ return _c
597+}
598+
599+func (_c *MergeGit_MergeBase_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, a domain.ObjectID, b domain.ObjectID) (domain.ObjectID, error)) *MergeGit_MergeBase_Call {
600+ _c.Call.Return(run)
601+ return _c
602+}
603+
604+// UpdateBranch provides a mock function for the type MergeGit
605+func (_mock *MergeGit) UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID, new domain.ObjectID) error {
606+ ret := _mock.Called(ctx, repo, branch, old, new)
607+
608+ if len(ret) == 0 {
609+ panic("no return value specified for UpdateBranch")
610+ }
611+
612+ var r0 error
613+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, domain.ObjectID, domain.ObjectID) error); ok {
614+ r0 = returnFunc(ctx, repo, branch, old, new)
615+ } else {
616+ r0 = ret.Error(0)
617+ }
618+ return r0
619+}
620+
621+// MergeGit_UpdateBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBranch'
622+type MergeGit_UpdateBranch_Call struct {
623+ *mock.Call
624+}
625+
626+// UpdateBranch is a helper method to define mock.On call
627+// - ctx context.Context
628+// - repo domain.RepoName
629+// - branch string
630+// - old domain.ObjectID
631+// - new domain.ObjectID
632+func (_e *MergeGit_Expecter) UpdateBranch(ctx any, repo any, branch any, old any, new any) *MergeGit_UpdateBranch_Call {
633+ return &MergeGit_UpdateBranch_Call{Call: _e.mock.On("UpdateBranch", ctx, repo, branch, old, new)}
634+}
635+
636+func (_c *MergeGit_UpdateBranch_Call) Run(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID, new domain.ObjectID)) *MergeGit_UpdateBranch_Call {
637+ _c.Call.Run(func(args mock.Arguments) {
638+ var arg0 context.Context
639+ if args[0] != nil {
640+ arg0 = args[0].(context.Context)
641+ }
642+ var arg1 domain.RepoName
643+ if args[1] != nil {
644+ arg1 = args[1].(domain.RepoName)
645+ }
646+ var arg2 string
647+ if args[2] != nil {
648+ arg2 = args[2].(string)
649+ }
650+ var arg3 domain.ObjectID
651+ if args[3] != nil {
652+ arg3 = args[3].(domain.ObjectID)
653+ }
654+ var arg4 domain.ObjectID
655+ if args[4] != nil {
656+ arg4 = args[4].(domain.ObjectID)
657+ }
658+ run(
659+ arg0,
660+ arg1,
661+ arg2,
662+ arg3,
663+ arg4,
664+ )
665+ })
666+ return _c
667+}
668+
669+func (_c *MergeGit_UpdateBranch_Call) Return(err error) *MergeGit_UpdateBranch_Call {
670+ _c.Call.Return(err)
671+ return _c
672+}
673+
674+func (_c *MergeGit_UpdateBranch_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, branch string, old domain.ObjectID, new domain.ObjectID) error) *MergeGit_UpdateBranch_Call {
675+ _c.Call.Return(run)
676+ return _c
677+}
678+
679+// WriteCommit provides a mock function for the type MergeGit
680+func (_mock *MergeGit) WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error) {
681+ ret := _mock.Called(ctx, repo, object)
682+
683+ if len(ret) == 0 {
684+ panic("no return value specified for WriteCommit")
685+ }
686+
687+ var r0 domain.ObjectID
688+ var r1 error
689+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, []byte) (domain.ObjectID, error)); ok {
690+ return returnFunc(ctx, repo, object)
691+ }
692+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, []byte) domain.ObjectID); ok {
693+ r0 = returnFunc(ctx, repo, object)
694+ } else {
695+ r0 = ret.Get(0).(domain.ObjectID)
696+ }
697+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, []byte) error); ok {
698+ r1 = returnFunc(ctx, repo, object)
699+ } else {
700+ r1 = ret.Error(1)
701+ }
702+ return r0, r1
703+}
704+
705+// MergeGit_WriteCommit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteCommit'
706+type MergeGit_WriteCommit_Call struct {
707+ *mock.Call
708+}
709+
710+// WriteCommit is a helper method to define mock.On call
711+// - ctx context.Context
712+// - repo domain.RepoName
713+// - object []byte
714+func (_e *MergeGit_Expecter) WriteCommit(ctx any, repo any, object any) *MergeGit_WriteCommit_Call {
715+ return &MergeGit_WriteCommit_Call{Call: _e.mock.On("WriteCommit", ctx, repo, object)}
716+}
717+
718+func (_c *MergeGit_WriteCommit_Call) Run(run func(ctx context.Context, repo domain.RepoName, object []byte)) *MergeGit_WriteCommit_Call {
719+ _c.Call.Run(func(args mock.Arguments) {
720+ var arg0 context.Context
721+ if args[0] != nil {
722+ arg0 = args[0].(context.Context)
723+ }
724+ var arg1 domain.RepoName
725+ if args[1] != nil {
726+ arg1 = args[1].(domain.RepoName)
727+ }
728+ var arg2 []byte
729+ if args[2] != nil {
730+ arg2 = args[2].([]byte)
731+ }
732+ run(
733+ arg0,
734+ arg1,
735+ arg2,
736+ )
737+ })
738+ return _c
739+}
740+
741+func (_c *MergeGit_WriteCommit_Call) Return(objectID domain.ObjectID, err error) *MergeGit_WriteCommit_Call {
742+ _c.Call.Return(objectID, err)
743+ return _c
744+}
745+
746+func (_c *MergeGit_WriteCommit_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)) *MergeGit_WriteCommit_Call {
747+ _c.Call.Return(run)
748+ return _c
749+}
A internal/ports/mocks/MergeRequestStore.go +667 -0
@@ -0,0 +1,667 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMergeRequestStore creates a new instance of MergeRequestStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMergeRequestStore(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MergeRequestStore {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MergeRequestStore{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MergeRequestStore is an autogenerated mock type for the MergeRequestStore type
38+type MergeRequestStore struct {
39+ mock.Mock
40+}
41+
42+type MergeRequestStore_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MergeRequestStore) EXPECT() *MergeRequestStore_Expecter {
47+ return &MergeRequestStore_Expecter{mock: &_m.Mock}
48+}
49+
50+// Create provides a mock function for the type MergeRequestStore
51+func (_mock *MergeRequestStore) Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error) {
52+ ret := _mock.Called(ctx, mr)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for Create")
56+ }
57+
58+ var r0 domain.MergeRequest
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest) (domain.MergeRequest, error)); ok {
61+ return returnFunc(ctx, mr)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest) domain.MergeRequest); ok {
64+ r0 = returnFunc(ctx, mr)
65+ } else {
66+ r0 = ret.Get(0).(domain.MergeRequest)
67+ }
68+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.MergeRequest) error); ok {
69+ r1 = returnFunc(ctx, mr)
70+ } else {
71+ r1 = ret.Error(1)
72+ }
73+ return r0, r1
74+}
75+
76+// MergeRequestStore_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
77+type MergeRequestStore_Create_Call struct {
78+ *mock.Call
79+}
80+
81+// Create is a helper method to define mock.On call
82+// - ctx context.Context
83+// - mr domain.MergeRequest
84+func (_e *MergeRequestStore_Expecter) Create(ctx any, mr any) *MergeRequestStore_Create_Call {
85+ return &MergeRequestStore_Create_Call{Call: _e.mock.On("Create", ctx, mr)}
86+}
87+
88+func (_c *MergeRequestStore_Create_Call) Run(run func(ctx context.Context, mr domain.MergeRequest)) *MergeRequestStore_Create_Call {
89+ _c.Call.Run(func(args mock.Arguments) {
90+ var arg0 context.Context
91+ if args[0] != nil {
92+ arg0 = args[0].(context.Context)
93+ }
94+ var arg1 domain.MergeRequest
95+ if args[1] != nil {
96+ arg1 = args[1].(domain.MergeRequest)
97+ }
98+ run(
99+ arg0,
100+ arg1,
101+ )
102+ })
103+ return _c
104+}
105+
106+func (_c *MergeRequestStore_Create_Call) Return(mergeRequest domain.MergeRequest, err error) *MergeRequestStore_Create_Call {
107+ _c.Call.Return(mergeRequest, err)
108+ return _c
109+}
110+
111+func (_c *MergeRequestStore_Create_Call) RunAndReturn(run func(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error)) *MergeRequestStore_Create_Call {
112+ _c.Call.Return(run)
113+ return _c
114+}
115+
116+// Get provides a mock function for the type MergeRequestStore
117+func (_mock *MergeRequestStore) Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error) {
118+ ret := _mock.Called(ctx, repo, number)
119+
120+ if len(ret) == 0 {
121+ panic("no return value specified for Get")
122+ }
123+
124+ var r0 domain.MergeRequest
125+ var r1 error
126+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) (domain.MergeRequest, error)); ok {
127+ return returnFunc(ctx, repo, number)
128+ }
129+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) domain.MergeRequest); ok {
130+ r0 = returnFunc(ctx, repo, number)
131+ } else {
132+ r0 = ret.Get(0).(domain.MergeRequest)
133+ }
134+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
135+ r1 = returnFunc(ctx, repo, number)
136+ } else {
137+ r1 = ret.Error(1)
138+ }
139+ return r0, r1
140+}
141+
142+// MergeRequestStore_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
143+type MergeRequestStore_Get_Call struct {
144+ *mock.Call
145+}
146+
147+// Get is a helper method to define mock.On call
148+// - ctx context.Context
149+// - repo domain.RepoName
150+// - number int64
151+func (_e *MergeRequestStore_Expecter) Get(ctx any, repo any, number any) *MergeRequestStore_Get_Call {
152+ return &MergeRequestStore_Get_Call{Call: _e.mock.On("Get", ctx, repo, number)}
153+}
154+
155+func (_c *MergeRequestStore_Get_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *MergeRequestStore_Get_Call {
156+ _c.Call.Run(func(args mock.Arguments) {
157+ var arg0 context.Context
158+ if args[0] != nil {
159+ arg0 = args[0].(context.Context)
160+ }
161+ var arg1 domain.RepoName
162+ if args[1] != nil {
163+ arg1 = args[1].(domain.RepoName)
164+ }
165+ var arg2 int64
166+ if args[2] != nil {
167+ arg2 = args[2].(int64)
168+ }
169+ run(
170+ arg0,
171+ arg1,
172+ arg2,
173+ )
174+ })
175+ return _c
176+}
177+
178+func (_c *MergeRequestStore_Get_Call) Return(mergeRequest domain.MergeRequest, err error) *MergeRequestStore_Get_Call {
179+ _c.Call.Return(mergeRequest, err)
180+ return _c
181+}
182+
183+func (_c *MergeRequestStore_Get_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error)) *MergeRequestStore_Get_Call {
184+ _c.Call.Return(run)
185+ return _c
186+}
187+
188+// GetComment provides a mock function for the type MergeRequestStore
189+func (_mock *MergeRequestStore) GetComment(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error) {
190+ ret := _mock.Called(ctx, repo, number, id)
191+
192+ if len(ret) == 0 {
193+ panic("no return value specified for GetComment")
194+ }
195+
196+ var r0 domain.Comment
197+ var r1 error
198+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, int64) (domain.Comment, error)); ok {
199+ return returnFunc(ctx, repo, number, id)
200+ }
201+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, int64) domain.Comment); ok {
202+ r0 = returnFunc(ctx, repo, number, id)
203+ } else {
204+ r0 = ret.Get(0).(domain.Comment)
205+ }
206+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, int64) error); ok {
207+ r1 = returnFunc(ctx, repo, number, id)
208+ } else {
209+ r1 = ret.Error(1)
210+ }
211+ return r0, r1
212+}
213+
214+// MergeRequestStore_GetComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetComment'
215+type MergeRequestStore_GetComment_Call struct {
216+ *mock.Call
217+}
218+
219+// GetComment is a helper method to define mock.On call
220+// - ctx context.Context
221+// - repo domain.RepoName
222+// - number int64
223+// - id int64
224+func (_e *MergeRequestStore_Expecter) GetComment(ctx any, repo any, number any, id any) *MergeRequestStore_GetComment_Call {
225+ return &MergeRequestStore_GetComment_Call{Call: _e.mock.On("GetComment", ctx, repo, number, id)}
226+}
227+
228+func (_c *MergeRequestStore_GetComment_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64, id int64)) *MergeRequestStore_GetComment_Call {
229+ _c.Call.Run(func(args mock.Arguments) {
230+ var arg0 context.Context
231+ if args[0] != nil {
232+ arg0 = args[0].(context.Context)
233+ }
234+ var arg1 domain.RepoName
235+ if args[1] != nil {
236+ arg1 = args[1].(domain.RepoName)
237+ }
238+ var arg2 int64
239+ if args[2] != nil {
240+ arg2 = args[2].(int64)
241+ }
242+ var arg3 int64
243+ if args[3] != nil {
244+ arg3 = args[3].(int64)
245+ }
246+ run(
247+ arg0,
248+ arg1,
249+ arg2,
250+ arg3,
251+ )
252+ })
253+ return _c
254+}
255+
256+func (_c *MergeRequestStore_GetComment_Call) Return(comment domain.Comment, err error) *MergeRequestStore_GetComment_Call {
257+ _c.Call.Return(comment, err)
258+ return _c
259+}
260+
261+func (_c *MergeRequestStore_GetComment_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error)) *MergeRequestStore_GetComment_Call {
262+ _c.Call.Return(run)
263+ return _c
264+}
265+
266+// GetRead provides a mock function for the type MergeRequestStore
267+func (_mock *MergeRequestStore) GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error) {
268+ ret := _mock.Called(ctx, repo, number)
269+
270+ if len(ret) == 0 {
271+ panic("no return value specified for GetRead")
272+ }
273+
274+ var r0 domain.ObjectID
275+ var r1 error
276+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) (domain.ObjectID, error)); ok {
277+ return returnFunc(ctx, repo, number)
278+ }
279+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) domain.ObjectID); ok {
280+ r0 = returnFunc(ctx, repo, number)
281+ } else {
282+ r0 = ret.Get(0).(domain.ObjectID)
283+ }
284+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
285+ r1 = returnFunc(ctx, repo, number)
286+ } else {
287+ r1 = ret.Error(1)
288+ }
289+ return r0, r1
290+}
291+
292+// MergeRequestStore_GetRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRead'
293+type MergeRequestStore_GetRead_Call struct {
294+ *mock.Call
295+}
296+
297+// GetRead is a helper method to define mock.On call
298+// - ctx context.Context
299+// - repo domain.RepoName
300+// - number int64
301+func (_e *MergeRequestStore_Expecter) GetRead(ctx any, repo any, number any) *MergeRequestStore_GetRead_Call {
302+ return &MergeRequestStore_GetRead_Call{Call: _e.mock.On("GetRead", ctx, repo, number)}
303+}
304+
305+func (_c *MergeRequestStore_GetRead_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *MergeRequestStore_GetRead_Call {
306+ _c.Call.Run(func(args mock.Arguments) {
307+ var arg0 context.Context
308+ if args[0] != nil {
309+ arg0 = args[0].(context.Context)
310+ }
311+ var arg1 domain.RepoName
312+ if args[1] != nil {
313+ arg1 = args[1].(domain.RepoName)
314+ }
315+ var arg2 int64
316+ if args[2] != nil {
317+ arg2 = args[2].(int64)
318+ }
319+ run(
320+ arg0,
321+ arg1,
322+ arg2,
323+ )
324+ })
325+ return _c
326+}
327+
328+func (_c *MergeRequestStore_GetRead_Call) Return(objectID domain.ObjectID, err error) *MergeRequestStore_GetRead_Call {
329+ _c.Call.Return(objectID, err)
330+ return _c
331+}
332+
333+func (_c *MergeRequestStore_GetRead_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error)) *MergeRequestStore_GetRead_Call {
334+ _c.Call.Return(run)
335+ return _c
336+}
337+
338+// List provides a mock function for the type MergeRequestStore
339+func (_mock *MergeRequestStore) List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error) {
340+ ret := _mock.Called(ctx, repo)
341+
342+ if len(ret) == 0 {
343+ panic("no return value specified for List")
344+ }
345+
346+ var r0 []domain.MergeRequest
347+ var r1 error
348+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]domain.MergeRequest, error)); ok {
349+ return returnFunc(ctx, repo)
350+ }
351+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []domain.MergeRequest); ok {
352+ r0 = returnFunc(ctx, repo)
353+ } else {
354+ if ret.Get(0) != nil {
355+ r0 = ret.Get(0).([]domain.MergeRequest)
356+ }
357+ }
358+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
359+ r1 = returnFunc(ctx, repo)
360+ } else {
361+ r1 = ret.Error(1)
362+ }
363+ return r0, r1
364+}
365+
366+// MergeRequestStore_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
367+type MergeRequestStore_List_Call struct {
368+ *mock.Call
369+}
370+
371+// List is a helper method to define mock.On call
372+// - ctx context.Context
373+// - repo domain.RepoName
374+func (_e *MergeRequestStore_Expecter) List(ctx any, repo any) *MergeRequestStore_List_Call {
375+ return &MergeRequestStore_List_Call{Call: _e.mock.On("List", ctx, repo)}
376+}
377+
378+func (_c *MergeRequestStore_List_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *MergeRequestStore_List_Call {
379+ _c.Call.Run(func(args mock.Arguments) {
380+ var arg0 context.Context
381+ if args[0] != nil {
382+ arg0 = args[0].(context.Context)
383+ }
384+ var arg1 domain.RepoName
385+ if args[1] != nil {
386+ arg1 = args[1].(domain.RepoName)
387+ }
388+ run(
389+ arg0,
390+ arg1,
391+ )
392+ })
393+ return _c
394+}
395+
396+func (_c *MergeRequestStore_List_Call) Return(mergeRequests []domain.MergeRequest, err error) *MergeRequestStore_List_Call {
397+ _c.Call.Return(mergeRequests, err)
398+ return _c
399+}
400+
401+func (_c *MergeRequestStore_List_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)) *MergeRequestStore_List_Call {
402+ _c.Call.Return(run)
403+ return _c
404+}
405+
406+// ListComments provides a mock function for the type MergeRequestStore
407+func (_mock *MergeRequestStore) ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error) {
408+ ret := _mock.Called(ctx, repo, number)
409+
410+ if len(ret) == 0 {
411+ panic("no return value specified for ListComments")
412+ }
413+
414+ var r0 []domain.Comment
415+ var r1 error
416+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.Comment, error)); ok {
417+ return returnFunc(ctx, repo, number)
418+ }
419+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.Comment); ok {
420+ r0 = returnFunc(ctx, repo, number)
421+ } else {
422+ if ret.Get(0) != nil {
423+ r0 = ret.Get(0).([]domain.Comment)
424+ }
425+ }
426+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
427+ r1 = returnFunc(ctx, repo, number)
428+ } else {
429+ r1 = ret.Error(1)
430+ }
431+ return r0, r1
432+}
433+
434+// MergeRequestStore_ListComments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListComments'
435+type MergeRequestStore_ListComments_Call struct {
436+ *mock.Call
437+}
438+
439+// ListComments is a helper method to define mock.On call
440+// - ctx context.Context
441+// - repo domain.RepoName
442+// - number int64
443+func (_e *MergeRequestStore_Expecter) ListComments(ctx any, repo any, number any) *MergeRequestStore_ListComments_Call {
444+ return &MergeRequestStore_ListComments_Call{Call: _e.mock.On("ListComments", ctx, repo, number)}
445+}
446+
447+func (_c *MergeRequestStore_ListComments_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *MergeRequestStore_ListComments_Call {
448+ _c.Call.Run(func(args mock.Arguments) {
449+ var arg0 context.Context
450+ if args[0] != nil {
451+ arg0 = args[0].(context.Context)
452+ }
453+ var arg1 domain.RepoName
454+ if args[1] != nil {
455+ arg1 = args[1].(domain.RepoName)
456+ }
457+ var arg2 int64
458+ if args[2] != nil {
459+ arg2 = args[2].(int64)
460+ }
461+ run(
462+ arg0,
463+ arg1,
464+ arg2,
465+ )
466+ })
467+ return _c
468+}
469+
470+func (_c *MergeRequestStore_ListComments_Call) Return(comments []domain.Comment, err error) *MergeRequestStore_ListComments_Call {
471+ _c.Call.Return(comments, err)
472+ return _c
473+}
474+
475+func (_c *MergeRequestStore_ListComments_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error)) *MergeRequestStore_ListComments_Call {
476+ _c.Call.Return(run)
477+ return _c
478+}
479+
480+// SaveComment provides a mock function for the type MergeRequestStore
481+func (_mock *MergeRequestStore) SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error {
482+ ret := _mock.Called(ctx, mr, c)
483+
484+ if len(ret) == 0 {
485+ panic("no return value specified for SaveComment")
486+ }
487+
488+ var r0 error
489+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest, domain.Comment) error); ok {
490+ r0 = returnFunc(ctx, mr, c)
491+ } else {
492+ r0 = ret.Error(0)
493+ }
494+ return r0
495+}
496+
497+// MergeRequestStore_SaveComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveComment'
498+type MergeRequestStore_SaveComment_Call struct {
499+ *mock.Call
500+}
501+
502+// SaveComment is a helper method to define mock.On call
503+// - ctx context.Context
504+// - mr domain.MergeRequest
505+// - c domain.Comment
506+func (_e *MergeRequestStore_Expecter) SaveComment(ctx any, mr any, c any) *MergeRequestStore_SaveComment_Call {
507+ return &MergeRequestStore_SaveComment_Call{Call: _e.mock.On("SaveComment", ctx, mr, c)}
508+}
509+
510+func (_c *MergeRequestStore_SaveComment_Call) Run(run func(ctx context.Context, mr domain.MergeRequest, c domain.Comment)) *MergeRequestStore_SaveComment_Call {
511+ _c.Call.Run(func(args mock.Arguments) {
512+ var arg0 context.Context
513+ if args[0] != nil {
514+ arg0 = args[0].(context.Context)
515+ }
516+ var arg1 domain.MergeRequest
517+ if args[1] != nil {
518+ arg1 = args[1].(domain.MergeRequest)
519+ }
520+ var arg2 domain.Comment
521+ if args[2] != nil {
522+ arg2 = args[2].(domain.Comment)
523+ }
524+ run(
525+ arg0,
526+ arg1,
527+ arg2,
528+ )
529+ })
530+ return _c
531+}
532+
533+func (_c *MergeRequestStore_SaveComment_Call) Return(err error) *MergeRequestStore_SaveComment_Call {
534+ _c.Call.Return(err)
535+ return _c
536+}
537+
538+func (_c *MergeRequestStore_SaveComment_Call) RunAndReturn(run func(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error) *MergeRequestStore_SaveComment_Call {
539+ _c.Call.Return(run)
540+ return _c
541+}
542+
543+// SetRead provides a mock function for the type MergeRequestStore
544+func (_mock *MergeRequestStore) SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error {
545+ ret := _mock.Called(ctx, repo, number, head)
546+
547+ if len(ret) == 0 {
548+ panic("no return value specified for SetRead")
549+ }
550+
551+ var r0 error
552+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, domain.ObjectID) error); ok {
553+ r0 = returnFunc(ctx, repo, number, head)
554+ } else {
555+ r0 = ret.Error(0)
556+ }
557+ return r0
558+}
559+
560+// MergeRequestStore_SetRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetRead'
561+type MergeRequestStore_SetRead_Call struct {
562+ *mock.Call
563+}
564+
565+// SetRead is a helper method to define mock.On call
566+// - ctx context.Context
567+// - repo domain.RepoName
568+// - number int64
569+// - head domain.ObjectID
570+func (_e *MergeRequestStore_Expecter) SetRead(ctx any, repo any, number any, head any) *MergeRequestStore_SetRead_Call {
571+ return &MergeRequestStore_SetRead_Call{Call: _e.mock.On("SetRead", ctx, repo, number, head)}
572+}
573+
574+func (_c *MergeRequestStore_SetRead_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID)) *MergeRequestStore_SetRead_Call {
575+ _c.Call.Run(func(args mock.Arguments) {
576+ var arg0 context.Context
577+ if args[0] != nil {
578+ arg0 = args[0].(context.Context)
579+ }
580+ var arg1 domain.RepoName
581+ if args[1] != nil {
582+ arg1 = args[1].(domain.RepoName)
583+ }
584+ var arg2 int64
585+ if args[2] != nil {
586+ arg2 = args[2].(int64)
587+ }
588+ var arg3 domain.ObjectID
589+ if args[3] != nil {
590+ arg3 = args[3].(domain.ObjectID)
591+ }
592+ run(
593+ arg0,
594+ arg1,
595+ arg2,
596+ arg3,
597+ )
598+ })
599+ return _c
600+}
601+
602+func (_c *MergeRequestStore_SetRead_Call) Return(err error) *MergeRequestStore_SetRead_Call {
603+ _c.Call.Return(err)
604+ return _c
605+}
606+
607+func (_c *MergeRequestStore_SetRead_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error) *MergeRequestStore_SetRead_Call {
608+ _c.Call.Return(run)
609+ return _c
610+}
611+
612+// Update provides a mock function for the type MergeRequestStore
613+func (_mock *MergeRequestStore) Update(ctx context.Context, mr domain.MergeRequest) error {
614+ ret := _mock.Called(ctx, mr)
615+
616+ if len(ret) == 0 {
617+ panic("no return value specified for Update")
618+ }
619+
620+ var r0 error
621+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest) error); ok {
622+ r0 = returnFunc(ctx, mr)
623+ } else {
624+ r0 = ret.Error(0)
625+ }
626+ return r0
627+}
628+
629+// MergeRequestStore_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
630+type MergeRequestStore_Update_Call struct {
631+ *mock.Call
632+}
633+
634+// Update is a helper method to define mock.On call
635+// - ctx context.Context
636+// - mr domain.MergeRequest
637+func (_e *MergeRequestStore_Expecter) Update(ctx any, mr any) *MergeRequestStore_Update_Call {
638+ return &MergeRequestStore_Update_Call{Call: _e.mock.On("Update", ctx, mr)}
639+}
640+
641+func (_c *MergeRequestStore_Update_Call) Run(run func(ctx context.Context, mr domain.MergeRequest)) *MergeRequestStore_Update_Call {
642+ _c.Call.Run(func(args mock.Arguments) {
643+ var arg0 context.Context
644+ if args[0] != nil {
645+ arg0 = args[0].(context.Context)
646+ }
647+ var arg1 domain.MergeRequest
648+ if args[1] != nil {
649+ arg1 = args[1].(domain.MergeRequest)
650+ }
651+ run(
652+ arg0,
653+ arg1,
654+ )
655+ })
656+ return _c
657+}
658+
659+func (_c *MergeRequestStore_Update_Call) Return(err error) *MergeRequestStore_Update_Call {
660+ _c.Call.Return(err)
661+ return _c
662+}
663+
664+func (_c *MergeRequestStore_Update_Call) RunAndReturn(run func(ctx context.Context, mr domain.MergeRequest) error) *MergeRequestStore_Update_Call {
665+ _c.Call.Return(run)
666+ return _c
667+}
@@ -0,0 +1,667 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewMergeRequestStore creates a new instance of MergeRequestStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewMergeRequestStore(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *MergeRequestStore {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &MergeRequestStore{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// MergeRequestStore is an autogenerated mock type for the MergeRequestStore type
38+type MergeRequestStore struct {
39+ mock.Mock
40+}
41+
42+type MergeRequestStore_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *MergeRequestStore) EXPECT() *MergeRequestStore_Expecter {
47+ return &MergeRequestStore_Expecter{mock: &_m.Mock}
48+}
49+
50+// Create provides a mock function for the type MergeRequestStore
51+func (_mock *MergeRequestStore) Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error) {
52+ ret := _mock.Called(ctx, mr)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for Create")
56+ }
57+
58+ var r0 domain.MergeRequest
59+ var r1 error
60+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest) (domain.MergeRequest, error)); ok {
61+ return returnFunc(ctx, mr)
62+ }
63+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest) domain.MergeRequest); ok {
64+ r0 = returnFunc(ctx, mr)
65+ } else {
66+ r0 = ret.Get(0).(domain.MergeRequest)
67+ }
68+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.MergeRequest) error); ok {
69+ r1 = returnFunc(ctx, mr)
70+ } else {
71+ r1 = ret.Error(1)
72+ }
73+ return r0, r1
74+}
75+
76+// MergeRequestStore_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
77+type MergeRequestStore_Create_Call struct {
78+ *mock.Call
79+}
80+
81+// Create is a helper method to define mock.On call
82+// - ctx context.Context
83+// - mr domain.MergeRequest
84+func (_e *MergeRequestStore_Expecter) Create(ctx any, mr any) *MergeRequestStore_Create_Call {
85+ return &MergeRequestStore_Create_Call{Call: _e.mock.On("Create", ctx, mr)}
86+}
87+
88+func (_c *MergeRequestStore_Create_Call) Run(run func(ctx context.Context, mr domain.MergeRequest)) *MergeRequestStore_Create_Call {
89+ _c.Call.Run(func(args mock.Arguments) {
90+ var arg0 context.Context
91+ if args[0] != nil {
92+ arg0 = args[0].(context.Context)
93+ }
94+ var arg1 domain.MergeRequest
95+ if args[1] != nil {
96+ arg1 = args[1].(domain.MergeRequest)
97+ }
98+ run(
99+ arg0,
100+ arg1,
101+ )
102+ })
103+ return _c
104+}
105+
106+func (_c *MergeRequestStore_Create_Call) Return(mergeRequest domain.MergeRequest, err error) *MergeRequestStore_Create_Call {
107+ _c.Call.Return(mergeRequest, err)
108+ return _c
109+}
110+
111+func (_c *MergeRequestStore_Create_Call) RunAndReturn(run func(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error)) *MergeRequestStore_Create_Call {
112+ _c.Call.Return(run)
113+ return _c
114+}
115+
116+// Get provides a mock function for the type MergeRequestStore
117+func (_mock *MergeRequestStore) Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error) {
118+ ret := _mock.Called(ctx, repo, number)
119+
120+ if len(ret) == 0 {
121+ panic("no return value specified for Get")
122+ }
123+
124+ var r0 domain.MergeRequest
125+ var r1 error
126+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) (domain.MergeRequest, error)); ok {
127+ return returnFunc(ctx, repo, number)
128+ }
129+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) domain.MergeRequest); ok {
130+ r0 = returnFunc(ctx, repo, number)
131+ } else {
132+ r0 = ret.Get(0).(domain.MergeRequest)
133+ }
134+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
135+ r1 = returnFunc(ctx, repo, number)
136+ } else {
137+ r1 = ret.Error(1)
138+ }
139+ return r0, r1
140+}
141+
142+// MergeRequestStore_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
143+type MergeRequestStore_Get_Call struct {
144+ *mock.Call
145+}
146+
147+// Get is a helper method to define mock.On call
148+// - ctx context.Context
149+// - repo domain.RepoName
150+// - number int64
151+func (_e *MergeRequestStore_Expecter) Get(ctx any, repo any, number any) *MergeRequestStore_Get_Call {
152+ return &MergeRequestStore_Get_Call{Call: _e.mock.On("Get", ctx, repo, number)}
153+}
154+
155+func (_c *MergeRequestStore_Get_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *MergeRequestStore_Get_Call {
156+ _c.Call.Run(func(args mock.Arguments) {
157+ var arg0 context.Context
158+ if args[0] != nil {
159+ arg0 = args[0].(context.Context)
160+ }
161+ var arg1 domain.RepoName
162+ if args[1] != nil {
163+ arg1 = args[1].(domain.RepoName)
164+ }
165+ var arg2 int64
166+ if args[2] != nil {
167+ arg2 = args[2].(int64)
168+ }
169+ run(
170+ arg0,
171+ arg1,
172+ arg2,
173+ )
174+ })
175+ return _c
176+}
177+
178+func (_c *MergeRequestStore_Get_Call) Return(mergeRequest domain.MergeRequest, err error) *MergeRequestStore_Get_Call {
179+ _c.Call.Return(mergeRequest, err)
180+ return _c
181+}
182+
183+func (_c *MergeRequestStore_Get_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error)) *MergeRequestStore_Get_Call {
184+ _c.Call.Return(run)
185+ return _c
186+}
187+
188+// GetComment provides a mock function for the type MergeRequestStore
189+func (_mock *MergeRequestStore) GetComment(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error) {
190+ ret := _mock.Called(ctx, repo, number, id)
191+
192+ if len(ret) == 0 {
193+ panic("no return value specified for GetComment")
194+ }
195+
196+ var r0 domain.Comment
197+ var r1 error
198+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, int64) (domain.Comment, error)); ok {
199+ return returnFunc(ctx, repo, number, id)
200+ }
201+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, int64) domain.Comment); ok {
202+ r0 = returnFunc(ctx, repo, number, id)
203+ } else {
204+ r0 = ret.Get(0).(domain.Comment)
205+ }
206+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, int64) error); ok {
207+ r1 = returnFunc(ctx, repo, number, id)
208+ } else {
209+ r1 = ret.Error(1)
210+ }
211+ return r0, r1
212+}
213+
214+// MergeRequestStore_GetComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetComment'
215+type MergeRequestStore_GetComment_Call struct {
216+ *mock.Call
217+}
218+
219+// GetComment is a helper method to define mock.On call
220+// - ctx context.Context
221+// - repo domain.RepoName
222+// - number int64
223+// - id int64
224+func (_e *MergeRequestStore_Expecter) GetComment(ctx any, repo any, number any, id any) *MergeRequestStore_GetComment_Call {
225+ return &MergeRequestStore_GetComment_Call{Call: _e.mock.On("GetComment", ctx, repo, number, id)}
226+}
227+
228+func (_c *MergeRequestStore_GetComment_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64, id int64)) *MergeRequestStore_GetComment_Call {
229+ _c.Call.Run(func(args mock.Arguments) {
230+ var arg0 context.Context
231+ if args[0] != nil {
232+ arg0 = args[0].(context.Context)
233+ }
234+ var arg1 domain.RepoName
235+ if args[1] != nil {
236+ arg1 = args[1].(domain.RepoName)
237+ }
238+ var arg2 int64
239+ if args[2] != nil {
240+ arg2 = args[2].(int64)
241+ }
242+ var arg3 int64
243+ if args[3] != nil {
244+ arg3 = args[3].(int64)
245+ }
246+ run(
247+ arg0,
248+ arg1,
249+ arg2,
250+ arg3,
251+ )
252+ })
253+ return _c
254+}
255+
256+func (_c *MergeRequestStore_GetComment_Call) Return(comment domain.Comment, err error) *MergeRequestStore_GetComment_Call {
257+ _c.Call.Return(comment, err)
258+ return _c
259+}
260+
261+func (_c *MergeRequestStore_GetComment_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error)) *MergeRequestStore_GetComment_Call {
262+ _c.Call.Return(run)
263+ return _c
264+}
265+
266+// GetRead provides a mock function for the type MergeRequestStore
267+func (_mock *MergeRequestStore) GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error) {
268+ ret := _mock.Called(ctx, repo, number)
269+
270+ if len(ret) == 0 {
271+ panic("no return value specified for GetRead")
272+ }
273+
274+ var r0 domain.ObjectID
275+ var r1 error
276+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) (domain.ObjectID, error)); ok {
277+ return returnFunc(ctx, repo, number)
278+ }
279+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) domain.ObjectID); ok {
280+ r0 = returnFunc(ctx, repo, number)
281+ } else {
282+ r0 = ret.Get(0).(domain.ObjectID)
283+ }
284+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
285+ r1 = returnFunc(ctx, repo, number)
286+ } else {
287+ r1 = ret.Error(1)
288+ }
289+ return r0, r1
290+}
291+
292+// MergeRequestStore_GetRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRead'
293+type MergeRequestStore_GetRead_Call struct {
294+ *mock.Call
295+}
296+
297+// GetRead is a helper method to define mock.On call
298+// - ctx context.Context
299+// - repo domain.RepoName
300+// - number int64
301+func (_e *MergeRequestStore_Expecter) GetRead(ctx any, repo any, number any) *MergeRequestStore_GetRead_Call {
302+ return &MergeRequestStore_GetRead_Call{Call: _e.mock.On("GetRead", ctx, repo, number)}
303+}
304+
305+func (_c *MergeRequestStore_GetRead_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *MergeRequestStore_GetRead_Call {
306+ _c.Call.Run(func(args mock.Arguments) {
307+ var arg0 context.Context
308+ if args[0] != nil {
309+ arg0 = args[0].(context.Context)
310+ }
311+ var arg1 domain.RepoName
312+ if args[1] != nil {
313+ arg1 = args[1].(domain.RepoName)
314+ }
315+ var arg2 int64
316+ if args[2] != nil {
317+ arg2 = args[2].(int64)
318+ }
319+ run(
320+ arg0,
321+ arg1,
322+ arg2,
323+ )
324+ })
325+ return _c
326+}
327+
328+func (_c *MergeRequestStore_GetRead_Call) Return(objectID domain.ObjectID, err error) *MergeRequestStore_GetRead_Call {
329+ _c.Call.Return(objectID, err)
330+ return _c
331+}
332+
333+func (_c *MergeRequestStore_GetRead_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error)) *MergeRequestStore_GetRead_Call {
334+ _c.Call.Return(run)
335+ return _c
336+}
337+
338+// List provides a mock function for the type MergeRequestStore
339+func (_mock *MergeRequestStore) List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error) {
340+ ret := _mock.Called(ctx, repo)
341+
342+ if len(ret) == 0 {
343+ panic("no return value specified for List")
344+ }
345+
346+ var r0 []domain.MergeRequest
347+ var r1 error
348+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]domain.MergeRequest, error)); ok {
349+ return returnFunc(ctx, repo)
350+ }
351+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []domain.MergeRequest); ok {
352+ r0 = returnFunc(ctx, repo)
353+ } else {
354+ if ret.Get(0) != nil {
355+ r0 = ret.Get(0).([]domain.MergeRequest)
356+ }
357+ }
358+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
359+ r1 = returnFunc(ctx, repo)
360+ } else {
361+ r1 = ret.Error(1)
362+ }
363+ return r0, r1
364+}
365+
366+// MergeRequestStore_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
367+type MergeRequestStore_List_Call struct {
368+ *mock.Call
369+}
370+
371+// List is a helper method to define mock.On call
372+// - ctx context.Context
373+// - repo domain.RepoName
374+func (_e *MergeRequestStore_Expecter) List(ctx any, repo any) *MergeRequestStore_List_Call {
375+ return &MergeRequestStore_List_Call{Call: _e.mock.On("List", ctx, repo)}
376+}
377+
378+func (_c *MergeRequestStore_List_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *MergeRequestStore_List_Call {
379+ _c.Call.Run(func(args mock.Arguments) {
380+ var arg0 context.Context
381+ if args[0] != nil {
382+ arg0 = args[0].(context.Context)
383+ }
384+ var arg1 domain.RepoName
385+ if args[1] != nil {
386+ arg1 = args[1].(domain.RepoName)
387+ }
388+ run(
389+ arg0,
390+ arg1,
391+ )
392+ })
393+ return _c
394+}
395+
396+func (_c *MergeRequestStore_List_Call) Return(mergeRequests []domain.MergeRequest, err error) *MergeRequestStore_List_Call {
397+ _c.Call.Return(mergeRequests, err)
398+ return _c
399+}
400+
401+func (_c *MergeRequestStore_List_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)) *MergeRequestStore_List_Call {
402+ _c.Call.Return(run)
403+ return _c
404+}
405+
406+// ListComments provides a mock function for the type MergeRequestStore
407+func (_mock *MergeRequestStore) ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error) {
408+ ret := _mock.Called(ctx, repo, number)
409+
410+ if len(ret) == 0 {
411+ panic("no return value specified for ListComments")
412+ }
413+
414+ var r0 []domain.Comment
415+ var r1 error
416+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.Comment, error)); ok {
417+ return returnFunc(ctx, repo, number)
418+ }
419+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.Comment); ok {
420+ r0 = returnFunc(ctx, repo, number)
421+ } else {
422+ if ret.Get(0) != nil {
423+ r0 = ret.Get(0).([]domain.Comment)
424+ }
425+ }
426+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
427+ r1 = returnFunc(ctx, repo, number)
428+ } else {
429+ r1 = ret.Error(1)
430+ }
431+ return r0, r1
432+}
433+
434+// MergeRequestStore_ListComments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListComments'
435+type MergeRequestStore_ListComments_Call struct {
436+ *mock.Call
437+}
438+
439+// ListComments is a helper method to define mock.On call
440+// - ctx context.Context
441+// - repo domain.RepoName
442+// - number int64
443+func (_e *MergeRequestStore_Expecter) ListComments(ctx any, repo any, number any) *MergeRequestStore_ListComments_Call {
444+ return &MergeRequestStore_ListComments_Call{Call: _e.mock.On("ListComments", ctx, repo, number)}
445+}
446+
447+func (_c *MergeRequestStore_ListComments_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *MergeRequestStore_ListComments_Call {
448+ _c.Call.Run(func(args mock.Arguments) {
449+ var arg0 context.Context
450+ if args[0] != nil {
451+ arg0 = args[0].(context.Context)
452+ }
453+ var arg1 domain.RepoName
454+ if args[1] != nil {
455+ arg1 = args[1].(domain.RepoName)
456+ }
457+ var arg2 int64
458+ if args[2] != nil {
459+ arg2 = args[2].(int64)
460+ }
461+ run(
462+ arg0,
463+ arg1,
464+ arg2,
465+ )
466+ })
467+ return _c
468+}
469+
470+func (_c *MergeRequestStore_ListComments_Call) Return(comments []domain.Comment, err error) *MergeRequestStore_ListComments_Call {
471+ _c.Call.Return(comments, err)
472+ return _c
473+}
474+
475+func (_c *MergeRequestStore_ListComments_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error)) *MergeRequestStore_ListComments_Call {
476+ _c.Call.Return(run)
477+ return _c
478+}
479+
480+// SaveComment provides a mock function for the type MergeRequestStore
481+func (_mock *MergeRequestStore) SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error {
482+ ret := _mock.Called(ctx, mr, c)
483+
484+ if len(ret) == 0 {
485+ panic("no return value specified for SaveComment")
486+ }
487+
488+ var r0 error
489+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest, domain.Comment) error); ok {
490+ r0 = returnFunc(ctx, mr, c)
491+ } else {
492+ r0 = ret.Error(0)
493+ }
494+ return r0
495+}
496+
497+// MergeRequestStore_SaveComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveComment'
498+type MergeRequestStore_SaveComment_Call struct {
499+ *mock.Call
500+}
501+
502+// SaveComment is a helper method to define mock.On call
503+// - ctx context.Context
504+// - mr domain.MergeRequest
505+// - c domain.Comment
506+func (_e *MergeRequestStore_Expecter) SaveComment(ctx any, mr any, c any) *MergeRequestStore_SaveComment_Call {
507+ return &MergeRequestStore_SaveComment_Call{Call: _e.mock.On("SaveComment", ctx, mr, c)}
508+}
509+
510+func (_c *MergeRequestStore_SaveComment_Call) Run(run func(ctx context.Context, mr domain.MergeRequest, c domain.Comment)) *MergeRequestStore_SaveComment_Call {
511+ _c.Call.Run(func(args mock.Arguments) {
512+ var arg0 context.Context
513+ if args[0] != nil {
514+ arg0 = args[0].(context.Context)
515+ }
516+ var arg1 domain.MergeRequest
517+ if args[1] != nil {
518+ arg1 = args[1].(domain.MergeRequest)
519+ }
520+ var arg2 domain.Comment
521+ if args[2] != nil {
522+ arg2 = args[2].(domain.Comment)
523+ }
524+ run(
525+ arg0,
526+ arg1,
527+ arg2,
528+ )
529+ })
530+ return _c
531+}
532+
533+func (_c *MergeRequestStore_SaveComment_Call) Return(err error) *MergeRequestStore_SaveComment_Call {
534+ _c.Call.Return(err)
535+ return _c
536+}
537+
538+func (_c *MergeRequestStore_SaveComment_Call) RunAndReturn(run func(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error) *MergeRequestStore_SaveComment_Call {
539+ _c.Call.Return(run)
540+ return _c
541+}
542+
543+// SetRead provides a mock function for the type MergeRequestStore
544+func (_mock *MergeRequestStore) SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error {
545+ ret := _mock.Called(ctx, repo, number, head)
546+
547+ if len(ret) == 0 {
548+ panic("no return value specified for SetRead")
549+ }
550+
551+ var r0 error
552+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, domain.ObjectID) error); ok {
553+ r0 = returnFunc(ctx, repo, number, head)
554+ } else {
555+ r0 = ret.Error(0)
556+ }
557+ return r0
558+}
559+
560+// MergeRequestStore_SetRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetRead'
561+type MergeRequestStore_SetRead_Call struct {
562+ *mock.Call
563+}
564+
565+// SetRead is a helper method to define mock.On call
566+// - ctx context.Context
567+// - repo domain.RepoName
568+// - number int64
569+// - head domain.ObjectID
570+func (_e *MergeRequestStore_Expecter) SetRead(ctx any, repo any, number any, head any) *MergeRequestStore_SetRead_Call {
571+ return &MergeRequestStore_SetRead_Call{Call: _e.mock.On("SetRead", ctx, repo, number, head)}
572+}
573+
574+func (_c *MergeRequestStore_SetRead_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID)) *MergeRequestStore_SetRead_Call {
575+ _c.Call.Run(func(args mock.Arguments) {
576+ var arg0 context.Context
577+ if args[0] != nil {
578+ arg0 = args[0].(context.Context)
579+ }
580+ var arg1 domain.RepoName
581+ if args[1] != nil {
582+ arg1 = args[1].(domain.RepoName)
583+ }
584+ var arg2 int64
585+ if args[2] != nil {
586+ arg2 = args[2].(int64)
587+ }
588+ var arg3 domain.ObjectID
589+ if args[3] != nil {
590+ arg3 = args[3].(domain.ObjectID)
591+ }
592+ run(
593+ arg0,
594+ arg1,
595+ arg2,
596+ arg3,
597+ )
598+ })
599+ return _c
600+}
601+
602+func (_c *MergeRequestStore_SetRead_Call) Return(err error) *MergeRequestStore_SetRead_Call {
603+ _c.Call.Return(err)
604+ return _c
605+}
606+
607+func (_c *MergeRequestStore_SetRead_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error) *MergeRequestStore_SetRead_Call {
608+ _c.Call.Return(run)
609+ return _c
610+}
611+
612+// Update provides a mock function for the type MergeRequestStore
613+func (_mock *MergeRequestStore) Update(ctx context.Context, mr domain.MergeRequest) error {
614+ ret := _mock.Called(ctx, mr)
615+
616+ if len(ret) == 0 {
617+ panic("no return value specified for Update")
618+ }
619+
620+ var r0 error
621+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.MergeRequest) error); ok {
622+ r0 = returnFunc(ctx, mr)
623+ } else {
624+ r0 = ret.Error(0)
625+ }
626+ return r0
627+}
628+
629+// MergeRequestStore_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
630+type MergeRequestStore_Update_Call struct {
631+ *mock.Call
632+}
633+
634+// Update is a helper method to define mock.On call
635+// - ctx context.Context
636+// - mr domain.MergeRequest
637+func (_e *MergeRequestStore_Expecter) Update(ctx any, mr any) *MergeRequestStore_Update_Call {
638+ return &MergeRequestStore_Update_Call{Call: _e.mock.On("Update", ctx, mr)}
639+}
640+
641+func (_c *MergeRequestStore_Update_Call) Run(run func(ctx context.Context, mr domain.MergeRequest)) *MergeRequestStore_Update_Call {
642+ _c.Call.Run(func(args mock.Arguments) {
643+ var arg0 context.Context
644+ if args[0] != nil {
645+ arg0 = args[0].(context.Context)
646+ }
647+ var arg1 domain.MergeRequest
648+ if args[1] != nil {
649+ arg1 = args[1].(domain.MergeRequest)
650+ }
651+ run(
652+ arg0,
653+ arg1,
654+ )
655+ })
656+ return _c
657+}
658+
659+func (_c *MergeRequestStore_Update_Call) Return(err error) *MergeRequestStore_Update_Call {
660+ _c.Call.Return(err)
661+ return _c
662+}
663+
664+func (_c *MergeRequestStore_Update_Call) RunAndReturn(run func(ctx context.Context, mr domain.MergeRequest) error) *MergeRequestStore_Update_Call {
665+ _c.Call.Return(run)
666+ return _c
667+}
M internal/ports/ports.go +81 -1
@@ -28,7 +28,10 @@ type RepoMetaStore interface {
2828 // there is none and domain.ErrConflict if another write changed the
2929 // stored repository since it was read.
3030 Update(ctx context.Context, repo domain.Repo) error
31- // Delete removes the repository. Deleting a missing one is not an error.
31+ // Delete removes the repository and everything keyed to it: its merge
32+ // requests, their comments and read positions, and its number sequence.
33+ // A repository created again under the same name starts empty, with its
34+ // numbers starting at 1. Deleting a missing one is not an error.
3235 Delete(ctx context.Context, name domain.RepoName) error
3336 }
3437
@@ -185,6 +188,83 @@ type TokenStore interface {
185188 Touch(ctx context.Context, id string, at time.Time) error
186189 }
187190
191+// MergeRequestStore keeps the merge requests of the repositories, their
192+// comments and the read position of the reviewer. A repository has no entry
193+// until its first merge request; the store does not check that it exists.
194+type MergeRequestStore interface {
195+ // Create stores the merge request under the next number of its
196+ // repository, which it sets, and returns it. Numbers start at 1 and are
197+ // never reused, even by a concurrent Create.
198+ Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error)
199+ // Get returns the merge request, or domain.ErrNotFound. Its Version
200+ // tells Update which state it was read at.
201+ Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error)
202+ // List returns every merge request of the repository, highest number
203+ // first.
204+ List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
205+ // Update replaces the stored merge request. It returns domain.ErrNotFound
206+ // if there is none and domain.ErrConflict if another write changed it
207+ // since mr was read.
208+ Update(ctx context.Context, mr domain.MergeRequest) error
209+ // ListComments returns the comments of the merge request, oldest first.
210+ ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error)
211+ // GetComment returns one comment, or domain.ErrNotFound.
212+ GetComment(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error)
213+ // SaveComment writes a comment and the merge request it belongs to in one
214+ // batch: a new comment (Version 0) must not exist yet, an existing one
215+ // must be unchanged since it was read, and so must mr. It returns
216+ // domain.ErrConflict otherwise. The caller sets mr.CommentSeq and the
217+ // comment's ID for a new comment, and mr.UpdatedAt.
218+ SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error
219+ // GetRead returns the head the reviewer last marked read, or the zero
220+ // ObjectID when there is none.
221+ GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error)
222+ // SetRead records the head the reviewer has read.
223+ SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error
224+}
225+
226+// MergeGit is what merge requests need from the Git repositories beyond
227+// RepoReader. Every method returns domain.ErrNotFound when the repository has
228+// no directory.
229+type MergeGit interface {
230+ // BranchHead returns the commit refs/heads/<branch> points to, or
231+ // domain.ErrNotFound when there is no such branch.
232+ BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)
233+ // HasCommit reports whether the repository holds that commit.
234+ HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)
235+ // MergeBase returns the best common ancestor of two commits, or
236+ // domain.ErrNotFound when they share no history.
237+ MergeBase(ctx context.Context, repo domain.RepoName, a, b domain.ObjectID) (domain.ObjectID, error)
238+ // IsAncestor reports whether ancestor is reachable from descendant, a
239+ // commit being its own ancestor.
240+ IsAncestor(ctx context.Context, repo domain.RepoName, ancestor, descendant domain.ObjectID) (bool, error)
241+ // ListRange returns the commits reachable from head and not from exclude
242+ // ("exclude..head"), newest first, at most limit; more is true when the
243+ // range holds more. A zero exclude means the whole history of head.
244+ ListRange(ctx context.Context, repo domain.RepoName, exclude, head domain.ObjectID, limit int) (commits []domain.Commit, more bool, err error)
245+ // DiffCommits returns the statistics and the diff from commit base to
246+ // commit head (renames detected), bounded by limits.
247+ DiffCommits(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error)
248+ // CommitTree returns the tree of a commit.
249+ CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)
250+ // WriteCommit stores a commit object, given as the text of the object,
251+ // and returns its id.
252+ WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)
253+ // UpdateBranch moves refs/heads/<branch> from old to new. It returns
254+ // domain.ErrConflict when the branch no longer points to old.
255+ UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, new domain.ObjectID) error
256+}
257+
258+// CommitSigner signs the commits ermite creates (ADR 0022). The private key
259+// never leaves it.
260+type CommitSigner interface {
261+ // PublicKey returns the public key and its fingerprint.
262+ PublicKey() domain.SigningPublicKey
263+ // Sign signs the text of a commit object and returns the armored SSH
264+ // signature (namespace "git") to put in its gpgsig header.
265+ Sign(payload []byte) (string, error)
266+}
267+
188268 // EventBus fans events out to in-process subscribers.
189269 type EventBus interface {
190270 // Publish hands the event to every subscriber without blocking. A
@@ -28,7 +28,10 @@ type RepoMetaStore interface {
28 // there is none and domain.ErrConflict if another write changed the28 // there is none and domain.ErrConflict if another write changed the
29 // stored repository since it was read.29 // stored repository since it was read.
30 Update(ctx context.Context, repo domain.Repo) error30 Update(ctx context.Context, repo domain.Repo) error
31- // Delete removes the repository. Deleting a missing one is not an error.31+ // Delete removes the repository and everything keyed to it: its merge
32+ // requests, their comments and read positions, and its number sequence.
33+ // A repository created again under the same name starts empty, with its
34+ // numbers starting at 1. Deleting a missing one is not an error.
32 Delete(ctx context.Context, name domain.RepoName) error35 Delete(ctx context.Context, name domain.RepoName) error
33 }36 }
34 37
@@ -185,6 +188,83 @@ type TokenStore interface {
185 Touch(ctx context.Context, id string, at time.Time) error188 Touch(ctx context.Context, id string, at time.Time) error
186 }189 }
187 190
191+// MergeRequestStore keeps the merge requests of the repositories, their
192+// comments and the read position of the reviewer. A repository has no entry
193+// until its first merge request; the store does not check that it exists.
194+type MergeRequestStore interface {
195+ // Create stores the merge request under the next number of its
196+ // repository, which it sets, and returns it. Numbers start at 1 and are
197+ // never reused, even by a concurrent Create.
198+ Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error)
199+ // Get returns the merge request, or domain.ErrNotFound. Its Version
200+ // tells Update which state it was read at.
201+ Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error)
202+ // List returns every merge request of the repository, highest number
203+ // first.
204+ List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error)
205+ // Update replaces the stored merge request. It returns domain.ErrNotFound
206+ // if there is none and domain.ErrConflict if another write changed it
207+ // since mr was read.
208+ Update(ctx context.Context, mr domain.MergeRequest) error
209+ // ListComments returns the comments of the merge request, oldest first.
210+ ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error)
211+ // GetComment returns one comment, or domain.ErrNotFound.
212+ GetComment(ctx context.Context, repo domain.RepoName, number int64, id int64) (domain.Comment, error)
213+ // SaveComment writes a comment and the merge request it belongs to in one
214+ // batch: a new comment (Version 0) must not exist yet, an existing one
215+ // must be unchanged since it was read, and so must mr. It returns
216+ // domain.ErrConflict otherwise. The caller sets mr.CommentSeq and the
217+ // comment's ID for a new comment, and mr.UpdatedAt.
218+ SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error
219+ // GetRead returns the head the reviewer last marked read, or the zero
220+ // ObjectID when there is none.
221+ GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error)
222+ // SetRead records the head the reviewer has read.
223+ SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error
224+}
225+
226+// MergeGit is what merge requests need from the Git repositories beyond
227+// RepoReader. Every method returns domain.ErrNotFound when the repository has
228+// no directory.
229+type MergeGit interface {
230+ // BranchHead returns the commit refs/heads/<branch> points to, or
231+ // domain.ErrNotFound when there is no such branch.
232+ BranchHead(ctx context.Context, repo domain.RepoName, branch string) (domain.ObjectID, error)
233+ // HasCommit reports whether the repository holds that commit.
234+ HasCommit(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (bool, error)
235+ // MergeBase returns the best common ancestor of two commits, or
236+ // domain.ErrNotFound when they share no history.
237+ MergeBase(ctx context.Context, repo domain.RepoName, a, b domain.ObjectID) (domain.ObjectID, error)
238+ // IsAncestor reports whether ancestor is reachable from descendant, a
239+ // commit being its own ancestor.
240+ IsAncestor(ctx context.Context, repo domain.RepoName, ancestor, descendant domain.ObjectID) (bool, error)
241+ // ListRange returns the commits reachable from head and not from exclude
242+ // ("exclude..head"), newest first, at most limit; more is true when the
243+ // range holds more. A zero exclude means the whole history of head.
244+ ListRange(ctx context.Context, repo domain.RepoName, exclude, head domain.ObjectID, limit int) (commits []domain.Commit, more bool, err error)
245+ // DiffCommits returns the statistics and the diff from commit base to
246+ // commit head (renames detected), bounded by limits.
247+ DiffCommits(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limits domain.DiffLimits) (domain.DiffStats, domain.Diff, error)
248+ // CommitTree returns the tree of a commit.
249+ CommitTree(ctx context.Context, repo domain.RepoName, id domain.ObjectID) (domain.ObjectID, error)
250+ // WriteCommit stores a commit object, given as the text of the object,
251+ // and returns its id.
252+ WriteCommit(ctx context.Context, repo domain.RepoName, object []byte) (domain.ObjectID, error)
253+ // UpdateBranch moves refs/heads/<branch> from old to new. It returns
254+ // domain.ErrConflict when the branch no longer points to old.
255+ UpdateBranch(ctx context.Context, repo domain.RepoName, branch string, old, new domain.ObjectID) error
256+}
257+
258+// CommitSigner signs the commits ermite creates (ADR 0022). The private key
259+// never leaves it.
260+type CommitSigner interface {
261+ // PublicKey returns the public key and its fingerprint.
262+ PublicKey() domain.SigningPublicKey
263+ // Sign signs the text of a commit object and returns the armored SSH
264+ // signature (namespace "git") to put in its gpgsig header.
265+ Sign(payload []byte) (string, error)
266+}
267+
188 // EventBus fans events out to in-process subscribers.268 // EventBus fans events out to in-process subscribers.
189 type EventBus interface {269 type EventBus interface {
190 // Publish hands the event to every subscriber without blocking. A270 // Publish hands the event to every subscriber without blocking. A

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close