detached 7288afe

feat(issues): resolved state, issue event and merge request issue link contracts

commit
7288afe3322bbe150e4c427f3b495c6a2ab79a62
author
brice · · 2026-10-22 18:08 UTC
committer
brice · · 2026-10-22 18:41 UTC
parent
1fb2421
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
22 files · +688 -103
M gen/ermite/storage/issue.pb.go +200 -27
@@ -34,7 +34,8 @@ const (
3434 IssueState_ISSUE_STATE_OPEN IssueState = 1
3535 IssueState_ISSUE_STATE_CLOSED IssueState = 2
3636 // Awaiting moderation. Not stored yet.
37- IssueState_ISSUE_STATE_PENDING IssueState = 3
37+ IssueState_ISSUE_STATE_PENDING IssueState = 3
38+ IssueState_ISSUE_STATE_RESOLVED IssueState = 4
3839 )
3940
4041 // Enum value maps for IssueState.
@@ -44,12 +45,14 @@ var (
4445 1: "ISSUE_STATE_OPEN",
4546 2: "ISSUE_STATE_CLOSED",
4647 3: "ISSUE_STATE_PENDING",
48+ 4: "ISSUE_STATE_RESOLVED",
4749 }
4850 IssueState_value = map[string]int32{
4951 "ISSUE_STATE_UNSPECIFIED": 0,
5052 "ISSUE_STATE_OPEN": 1,
5153 "ISSUE_STATE_CLOSED": 2,
5254 "ISSUE_STATE_PENDING": 3,
55+ "ISSUE_STATE_RESOLVED": 4,
5356 }
5457 )
5558
@@ -80,6 +83,64 @@ func (IssueState) EnumDescriptor() ([]byte, []int) {
8083 return file_ermite_storage_issue_proto_rawDescGZIP(), []int{0}
8184 }
8285
86+// IssueEventKind describes an issue history entry.
87+type IssueEventKind int32
88+
89+const (
90+ IssueEventKind_ISSUE_EVENT_KIND_UNSPECIFIED IssueEventKind = 0
91+ // Closes says whether this is a closing reference.
92+ IssueEventKind_ISSUE_EVENT_KIND_REFERENCED IssueEventKind = 1
93+ IssueEventKind_ISSUE_EVENT_KIND_CLOSED IssueEventKind = 2
94+ IssueEventKind_ISSUE_EVENT_KIND_REOPENED IssueEventKind = 3
95+ // Merge_request is set when a merge resolved the issue.
96+ IssueEventKind_ISSUE_EVENT_KIND_RESOLVED IssueEventKind = 4
97+)
98+
99+// Enum value maps for IssueEventKind.
100+var (
101+ IssueEventKind_name = map[int32]string{
102+ 0: "ISSUE_EVENT_KIND_UNSPECIFIED",
103+ 1: "ISSUE_EVENT_KIND_REFERENCED",
104+ 2: "ISSUE_EVENT_KIND_CLOSED",
105+ 3: "ISSUE_EVENT_KIND_REOPENED",
106+ 4: "ISSUE_EVENT_KIND_RESOLVED",
107+ }
108+ IssueEventKind_value = map[string]int32{
109+ "ISSUE_EVENT_KIND_UNSPECIFIED": 0,
110+ "ISSUE_EVENT_KIND_REFERENCED": 1,
111+ "ISSUE_EVENT_KIND_CLOSED": 2,
112+ "ISSUE_EVENT_KIND_REOPENED": 3,
113+ "ISSUE_EVENT_KIND_RESOLVED": 4,
114+ }
115+)
116+
117+func (x IssueEventKind) Enum() *IssueEventKind {
118+ p := new(IssueEventKind)
119+ *p = x
120+ return p
121+}
122+
123+func (x IssueEventKind) String() string {
124+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
125+}
126+
127+func (IssueEventKind) Descriptor() protoreflect.EnumDescriptor {
128+ return file_ermite_storage_issue_proto_enumTypes[1].Descriptor()
129+}
130+
131+func (IssueEventKind) Type() protoreflect.EnumType {
132+ return &file_ermite_storage_issue_proto_enumTypes[1]
133+}
134+
135+func (x IssueEventKind) Number() protoreflect.EnumNumber {
136+ return protoreflect.EnumNumber(x)
137+}
138+
139+// Deprecated: Use IssueEventKind.Descriptor instead.
140+func (IssueEventKind) EnumDescriptor() ([]byte, []int) {
141+ return file_ermite_storage_issue_proto_rawDescGZIP(), []int{1}
142+}
143+
83144 // Issue is stored under the key "is/<repo>/<number:010d>". The numbers come
84145 // from the sequence of the merge requests ("mrseq/<repo>"). The open ones are
85146 // indexed under "isopen/<repo>/<number:010d>".
@@ -97,7 +158,8 @@ type Issue struct {
97158 CommentSeq int64 `protobuf:"varint,8,opt,name=comment_seq,json=commentSeq,proto3" json:"comment_seq,omitempty"`
98159 // True for an issue approved from the public form: its text comes from
99160 // an anonymous visitor and is rendered as untrusted.
100- External bool `protobuf:"varint,9,opt,name=external,proto3" json:"external,omitempty"`
161+ External bool `protobuf:"varint,9,opt,name=external,proto3" json:"external,omitempty"`
162+ // Set only while RESOLVED by a merge.
101163 ClosedByMergeRequest int64 `protobuf:"varint,10,opt,name=closed_by_merge_request,json=closedByMergeRequest,proto3" json:"closed_by_merge_request,omitempty"`
102164 unknownFields protoimpl.UnknownFields
103165 sizeCache protoimpl.SizeCache
@@ -542,6 +604,97 @@ func (x *IssueKey) GetKey() []byte {
542604 return nil
543605 }
544606
607+// IssueEvent is stored under "isev/<repo>/<issue:010d>/<seq:010d>" and,
608+// when merge_request != 0, copied verbatim under
609+// "mrev/<repo>/<mr:010d>/<issue:010d>/<seq:010d>". Append-only: never
610+// updated or deleted except with the repository.
611+type IssueEvent struct {
612+ state protoimpl.MessageState `protogen:"open.v1"`
613+ Seq int64 `protobuf:"varint,1,opt,name=seq,proto3" json:"seq,omitempty"`
614+ Kind IssueEventKind `protobuf:"varint,2,opt,name=kind,proto3,enum=ermite.storage.IssueEventKind" json:"kind,omitempty"`
615+ // Zero if none.
616+ MergeRequest int64 `protobuf:"varint,3,opt,name=merge_request,json=mergeRequest,proto3" json:"merge_request,omitempty"`
617+ // REFERENCED only.
618+ Closes bool `protobuf:"varint,4,opt,name=closes,proto3" json:"closes,omitempty"`
619+ // Label, like Issue.author.
620+ Actor string `protobuf:"bytes,5,opt,name=actor,proto3" json:"actor,omitempty"`
621+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
622+ unknownFields protoimpl.UnknownFields
623+ sizeCache protoimpl.SizeCache
624+}
625+
626+func (x *IssueEvent) Reset() {
627+ *x = IssueEvent{}
628+ mi := &file_ermite_storage_issue_proto_msgTypes[7]
629+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
630+ ms.StoreMessageInfo(mi)
631+}
632+
633+func (x *IssueEvent) String() string {
634+ return protoimpl.X.MessageStringOf(x)
635+}
636+
637+func (*IssueEvent) ProtoMessage() {}
638+
639+func (x *IssueEvent) ProtoReflect() protoreflect.Message {
640+ mi := &file_ermite_storage_issue_proto_msgTypes[7]
641+ if x != nil {
642+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
643+ if ms.LoadMessageInfo() == nil {
644+ ms.StoreMessageInfo(mi)
645+ }
646+ return ms
647+ }
648+ return mi.MessageOf(x)
649+}
650+
651+// Deprecated: Use IssueEvent.ProtoReflect.Descriptor instead.
652+func (*IssueEvent) Descriptor() ([]byte, []int) {
653+ return file_ermite_storage_issue_proto_rawDescGZIP(), []int{7}
654+}
655+
656+func (x *IssueEvent) GetSeq() int64 {
657+ if x != nil {
658+ return x.Seq
659+ }
660+ return 0
661+}
662+
663+func (x *IssueEvent) GetKind() IssueEventKind {
664+ if x != nil {
665+ return x.Kind
666+ }
667+ return IssueEventKind_ISSUE_EVENT_KIND_UNSPECIFIED
668+}
669+
670+func (x *IssueEvent) GetMergeRequest() int64 {
671+ if x != nil {
672+ return x.MergeRequest
673+ }
674+ return 0
675+}
676+
677+func (x *IssueEvent) GetCloses() bool {
678+ if x != nil {
679+ return x.Closes
680+ }
681+ return false
682+}
683+
684+func (x *IssueEvent) GetActor() string {
685+ if x != nil {
686+ return x.Actor
687+ }
688+ return ""
689+}
690+
691+func (x *IssueEvent) GetCreatedAt() *timestamppb.Timestamp {
692+ if x != nil {
693+ return x.CreatedAt
694+ }
695+ return nil
696+}
697+
545698 var File_ermite_storage_issue_proto protoreflect.FileDescriptor
546699
547700 const file_ermite_storage_issue_proto_rawDesc = "" +
@@ -583,13 +736,29 @@ const file_ermite_storage_issue_proto_rawDesc = "" +
583736 "\bIssueBan\x120\n" +
584737 "\x05until\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x05until\"\x1c\n" +
585738 "\bIssueKey\x12\x10\n" +
586- "\x03key\x18\x01 \x01(\fR\x03key*p\n" +
739+ "\x03key\x18\x01 \x01(\fR\x03key\"\xe0\x01\n" +
740+ "\n" +
741+ "IssueEvent\x12\x10\n" +
742+ "\x03seq\x18\x01 \x01(\x03R\x03seq\x122\n" +
743+ "\x04kind\x18\x02 \x01(\x0e2\x1e.ermite.storage.IssueEventKindR\x04kind\x12#\n" +
744+ "\rmerge_request\x18\x03 \x01(\x03R\fmergeRequest\x12\x16\n" +
745+ "\x06closes\x18\x04 \x01(\bR\x06closes\x12\x14\n" +
746+ "\x05actor\x18\x05 \x01(\tR\x05actor\x129\n" +
747+ "\n" +
748+ "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt*\x8a\x01\n" +
587749 "\n" +
588750 "IssueState\x12\x1b\n" +
589751 "\x17ISSUE_STATE_UNSPECIFIED\x10\x00\x12\x14\n" +
590752 "\x10ISSUE_STATE_OPEN\x10\x01\x12\x16\n" +
591753 "\x12ISSUE_STATE_CLOSED\x10\x02\x12\x17\n" +
592- "\x13ISSUE_STATE_PENDING\x10\x03B/Z-bnema.dev/ermite/gen/ermite/storage;storagepbb\x06proto3"
754+ "\x13ISSUE_STATE_PENDING\x10\x03\x12\x18\n" +
755+ "\x14ISSUE_STATE_RESOLVED\x10\x04*\xae\x01\n" +
756+ "\x0eIssueEventKind\x12 \n" +
757+ "\x1cISSUE_EVENT_KIND_UNSPECIFIED\x10\x00\x12\x1f\n" +
758+ "\x1bISSUE_EVENT_KIND_REFERENCED\x10\x01\x12\x1b\n" +
759+ "\x17ISSUE_EVENT_KIND_CLOSED\x10\x02\x12\x1d\n" +
760+ "\x19ISSUE_EVENT_KIND_REOPENED\x10\x03\x12\x1d\n" +
761+ "\x19ISSUE_EVENT_KIND_RESOLVED\x10\x04B/Z-bnema.dev/ermite/gen/ermite/storage;storagepbb\x06proto3"
593762
594763 var (
595764 file_ermite_storage_issue_proto_rawDescOnce sync.Once
@@ -603,31 +772,35 @@ func file_ermite_storage_issue_proto_rawDescGZIP() []byte {
603772 return file_ermite_storage_issue_proto_rawDescData
604773 }
605774
606-var file_ermite_storage_issue_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
607-var file_ermite_storage_issue_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
775+var file_ermite_storage_issue_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
776+var file_ermite_storage_issue_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
608777 var file_ermite_storage_issue_proto_goTypes = []any{
609778 (IssueState)(0), // 0: ermite.storage.IssueState
610- (*Issue)(nil), // 1: ermite.storage.Issue
611- (*IssueLink)(nil), // 2: ermite.storage.IssueLink
612- (*MergeRequestReferences)(nil), // 3: ermite.storage.MergeRequestReferences
613- (*IssueComment)(nil), // 4: ermite.storage.IssueComment
614- (*PendingIssue)(nil), // 5: ermite.storage.PendingIssue
615- (*IssueBan)(nil), // 6: ermite.storage.IssueBan
616- (*IssueKey)(nil), // 7: ermite.storage.IssueKey
617- (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
779+ (IssueEventKind)(0), // 1: ermite.storage.IssueEventKind
780+ (*Issue)(nil), // 2: ermite.storage.Issue
781+ (*IssueLink)(nil), // 3: ermite.storage.IssueLink
782+ (*MergeRequestReferences)(nil), // 4: ermite.storage.MergeRequestReferences
783+ (*IssueComment)(nil), // 5: ermite.storage.IssueComment
784+ (*PendingIssue)(nil), // 6: ermite.storage.PendingIssue
785+ (*IssueBan)(nil), // 7: ermite.storage.IssueBan
786+ (*IssueKey)(nil), // 8: ermite.storage.IssueKey
787+ (*IssueEvent)(nil), // 9: ermite.storage.IssueEvent
788+ (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
618789 }
619790 var file_ermite_storage_issue_proto_depIdxs = []int32{
620- 0, // 0: ermite.storage.Issue.state:type_name -> ermite.storage.IssueState
621- 8, // 1: ermite.storage.Issue.created_at:type_name -> google.protobuf.Timestamp
622- 8, // 2: ermite.storage.Issue.updated_at:type_name -> google.protobuf.Timestamp
623- 8, // 3: ermite.storage.IssueComment.created_at:type_name -> google.protobuf.Timestamp
624- 8, // 4: ermite.storage.PendingIssue.created_at:type_name -> google.protobuf.Timestamp
625- 8, // 5: ermite.storage.IssueBan.until:type_name -> google.protobuf.Timestamp
626- 6, // [6:6] is the sub-list for method output_type
627- 6, // [6:6] is the sub-list for method input_type
628- 6, // [6:6] is the sub-list for extension type_name
629- 6, // [6:6] is the sub-list for extension extendee
630- 0, // [0:6] is the sub-list for field type_name
791+ 0, // 0: ermite.storage.Issue.state:type_name -> ermite.storage.IssueState
792+ 10, // 1: ermite.storage.Issue.created_at:type_name -> google.protobuf.Timestamp
793+ 10, // 2: ermite.storage.Issue.updated_at:type_name -> google.protobuf.Timestamp
794+ 10, // 3: ermite.storage.IssueComment.created_at:type_name -> google.protobuf.Timestamp
795+ 10, // 4: ermite.storage.PendingIssue.created_at:type_name -> google.protobuf.Timestamp
796+ 10, // 5: ermite.storage.IssueBan.until:type_name -> google.protobuf.Timestamp
797+ 1, // 6: ermite.storage.IssueEvent.kind:type_name -> ermite.storage.IssueEventKind
798+ 10, // 7: ermite.storage.IssueEvent.created_at:type_name -> google.protobuf.Timestamp
799+ 8, // [8:8] is the sub-list for method output_type
800+ 8, // [8:8] is the sub-list for method input_type
801+ 8, // [8:8] is the sub-list for extension type_name
802+ 8, // [8:8] is the sub-list for extension extendee
803+ 0, // [0:8] is the sub-list for field type_name
631804 }
632805
633806 func init() { file_ermite_storage_issue_proto_init() }
@@ -640,8 +813,8 @@ func file_ermite_storage_issue_proto_init() {
640813 File: protoimpl.DescBuilder{
641814 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
642815 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_storage_issue_proto_rawDesc), len(file_ermite_storage_issue_proto_rawDesc)),
643- NumEnums: 1,
644- NumMessages: 7,
816+ NumEnums: 2,
817+ NumMessages: 8,
645818 NumExtensions: 0,
646819 NumServices: 0,
647820 },
@@ -34,7 +34,8 @@ const (
34 IssueState_ISSUE_STATE_OPEN IssueState = 134 IssueState_ISSUE_STATE_OPEN IssueState = 1
35 IssueState_ISSUE_STATE_CLOSED IssueState = 235 IssueState_ISSUE_STATE_CLOSED IssueState = 2
36 // Awaiting moderation. Not stored yet.36 // Awaiting moderation. Not stored yet.
37- IssueState_ISSUE_STATE_PENDING IssueState = 337+ IssueState_ISSUE_STATE_PENDING IssueState = 3
38+ IssueState_ISSUE_STATE_RESOLVED IssueState = 4
38 )39 )
39 40
40 // Enum value maps for IssueState.41 // Enum value maps for IssueState.
@@ -44,12 +45,14 @@ var (
44 1: "ISSUE_STATE_OPEN",45 1: "ISSUE_STATE_OPEN",
45 2: "ISSUE_STATE_CLOSED",46 2: "ISSUE_STATE_CLOSED",
46 3: "ISSUE_STATE_PENDING",47 3: "ISSUE_STATE_PENDING",
48+ 4: "ISSUE_STATE_RESOLVED",
47 }49 }
48 IssueState_value = map[string]int32{50 IssueState_value = map[string]int32{
49 "ISSUE_STATE_UNSPECIFIED": 0,51 "ISSUE_STATE_UNSPECIFIED": 0,
50 "ISSUE_STATE_OPEN": 1,52 "ISSUE_STATE_OPEN": 1,
51 "ISSUE_STATE_CLOSED": 2,53 "ISSUE_STATE_CLOSED": 2,
52 "ISSUE_STATE_PENDING": 3,54 "ISSUE_STATE_PENDING": 3,
55+ "ISSUE_STATE_RESOLVED": 4,
53 }56 }
54 )57 )
55 58
@@ -80,6 +83,64 @@ func (IssueState) EnumDescriptor() ([]byte, []int) {
80 return file_ermite_storage_issue_proto_rawDescGZIP(), []int{0}83 return file_ermite_storage_issue_proto_rawDescGZIP(), []int{0}
81 }84 }
82 85
86+// IssueEventKind describes an issue history entry.
87+type IssueEventKind int32
88+
89+const (
90+ IssueEventKind_ISSUE_EVENT_KIND_UNSPECIFIED IssueEventKind = 0
91+ // Closes says whether this is a closing reference.
92+ IssueEventKind_ISSUE_EVENT_KIND_REFERENCED IssueEventKind = 1
93+ IssueEventKind_ISSUE_EVENT_KIND_CLOSED IssueEventKind = 2
94+ IssueEventKind_ISSUE_EVENT_KIND_REOPENED IssueEventKind = 3
95+ // Merge_request is set when a merge resolved the issue.
96+ IssueEventKind_ISSUE_EVENT_KIND_RESOLVED IssueEventKind = 4
97+)
98+
99+// Enum value maps for IssueEventKind.
100+var (
101+ IssueEventKind_name = map[int32]string{
102+ 0: "ISSUE_EVENT_KIND_UNSPECIFIED",
103+ 1: "ISSUE_EVENT_KIND_REFERENCED",
104+ 2: "ISSUE_EVENT_KIND_CLOSED",
105+ 3: "ISSUE_EVENT_KIND_REOPENED",
106+ 4: "ISSUE_EVENT_KIND_RESOLVED",
107+ }
108+ IssueEventKind_value = map[string]int32{
109+ "ISSUE_EVENT_KIND_UNSPECIFIED": 0,
110+ "ISSUE_EVENT_KIND_REFERENCED": 1,
111+ "ISSUE_EVENT_KIND_CLOSED": 2,
112+ "ISSUE_EVENT_KIND_REOPENED": 3,
113+ "ISSUE_EVENT_KIND_RESOLVED": 4,
114+ }
115+)
116+
117+func (x IssueEventKind) Enum() *IssueEventKind {
118+ p := new(IssueEventKind)
119+ *p = x
120+ return p
121+}
122+
123+func (x IssueEventKind) String() string {
124+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
125+}
126+
127+func (IssueEventKind) Descriptor() protoreflect.EnumDescriptor {
128+ return file_ermite_storage_issue_proto_enumTypes[1].Descriptor()
129+}
130+
131+func (IssueEventKind) Type() protoreflect.EnumType {
132+ return &file_ermite_storage_issue_proto_enumTypes[1]
133+}
134+
135+func (x IssueEventKind) Number() protoreflect.EnumNumber {
136+ return protoreflect.EnumNumber(x)
137+}
138+
139+// Deprecated: Use IssueEventKind.Descriptor instead.
140+func (IssueEventKind) EnumDescriptor() ([]byte, []int) {
141+ return file_ermite_storage_issue_proto_rawDescGZIP(), []int{1}
142+}
143+
83 // Issue is stored under the key "is/<repo>/<number:010d>". The numbers come144 // Issue is stored under the key "is/<repo>/<number:010d>". The numbers come
84 // from the sequence of the merge requests ("mrseq/<repo>"). The open ones are145 // from the sequence of the merge requests ("mrseq/<repo>"). The open ones are
85 // indexed under "isopen/<repo>/<number:010d>".146 // indexed under "isopen/<repo>/<number:010d>".
@@ -97,7 +158,8 @@ type Issue struct {
97 CommentSeq int64 `protobuf:"varint,8,opt,name=comment_seq,json=commentSeq,proto3" json:"comment_seq,omitempty"`158 CommentSeq int64 `protobuf:"varint,8,opt,name=comment_seq,json=commentSeq,proto3" json:"comment_seq,omitempty"`
98 // True for an issue approved from the public form: its text comes from159 // True for an issue approved from the public form: its text comes from
99 // an anonymous visitor and is rendered as untrusted.160 // an anonymous visitor and is rendered as untrusted.
100- External bool `protobuf:"varint,9,opt,name=external,proto3" json:"external,omitempty"`161+ External bool `protobuf:"varint,9,opt,name=external,proto3" json:"external,omitempty"`
162+ // Set only while RESOLVED by a merge.
101 ClosedByMergeRequest int64 `protobuf:"varint,10,opt,name=closed_by_merge_request,json=closedByMergeRequest,proto3" json:"closed_by_merge_request,omitempty"`163 ClosedByMergeRequest int64 `protobuf:"varint,10,opt,name=closed_by_merge_request,json=closedByMergeRequest,proto3" json:"closed_by_merge_request,omitempty"`
102 unknownFields protoimpl.UnknownFields164 unknownFields protoimpl.UnknownFields
103 sizeCache protoimpl.SizeCache165 sizeCache protoimpl.SizeCache
@@ -542,6 +604,97 @@ func (x *IssueKey) GetKey() []byte {
542 return nil604 return nil
543 }605 }
544 606
607+// IssueEvent is stored under "isev/<repo>/<issue:010d>/<seq:010d>" and,
608+// when merge_request != 0, copied verbatim under
609+// "mrev/<repo>/<mr:010d>/<issue:010d>/<seq:010d>". Append-only: never
610+// updated or deleted except with the repository.
611+type IssueEvent struct {
612+ state protoimpl.MessageState `protogen:"open.v1"`
613+ Seq int64 `protobuf:"varint,1,opt,name=seq,proto3" json:"seq,omitempty"`
614+ Kind IssueEventKind `protobuf:"varint,2,opt,name=kind,proto3,enum=ermite.storage.IssueEventKind" json:"kind,omitempty"`
615+ // Zero if none.
616+ MergeRequest int64 `protobuf:"varint,3,opt,name=merge_request,json=mergeRequest,proto3" json:"merge_request,omitempty"`
617+ // REFERENCED only.
618+ Closes bool `protobuf:"varint,4,opt,name=closes,proto3" json:"closes,omitempty"`
619+ // Label, like Issue.author.
620+ Actor string `protobuf:"bytes,5,opt,name=actor,proto3" json:"actor,omitempty"`
621+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
622+ unknownFields protoimpl.UnknownFields
623+ sizeCache protoimpl.SizeCache
624+}
625+
626+func (x *IssueEvent) Reset() {
627+ *x = IssueEvent{}
628+ mi := &file_ermite_storage_issue_proto_msgTypes[7]
629+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
630+ ms.StoreMessageInfo(mi)
631+}
632+
633+func (x *IssueEvent) String() string {
634+ return protoimpl.X.MessageStringOf(x)
635+}
636+
637+func (*IssueEvent) ProtoMessage() {}
638+
639+func (x *IssueEvent) ProtoReflect() protoreflect.Message {
640+ mi := &file_ermite_storage_issue_proto_msgTypes[7]
641+ if x != nil {
642+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
643+ if ms.LoadMessageInfo() == nil {
644+ ms.StoreMessageInfo(mi)
645+ }
646+ return ms
647+ }
648+ return mi.MessageOf(x)
649+}
650+
651+// Deprecated: Use IssueEvent.ProtoReflect.Descriptor instead.
652+func (*IssueEvent) Descriptor() ([]byte, []int) {
653+ return file_ermite_storage_issue_proto_rawDescGZIP(), []int{7}
654+}
655+
656+func (x *IssueEvent) GetSeq() int64 {
657+ if x != nil {
658+ return x.Seq
659+ }
660+ return 0
661+}
662+
663+func (x *IssueEvent) GetKind() IssueEventKind {
664+ if x != nil {
665+ return x.Kind
666+ }
667+ return IssueEventKind_ISSUE_EVENT_KIND_UNSPECIFIED
668+}
669+
670+func (x *IssueEvent) GetMergeRequest() int64 {
671+ if x != nil {
672+ return x.MergeRequest
673+ }
674+ return 0
675+}
676+
677+func (x *IssueEvent) GetCloses() bool {
678+ if x != nil {
679+ return x.Closes
680+ }
681+ return false
682+}
683+
684+func (x *IssueEvent) GetActor() string {
685+ if x != nil {
686+ return x.Actor
687+ }
688+ return ""
689+}
690+
691+func (x *IssueEvent) GetCreatedAt() *timestamppb.Timestamp {
692+ if x != nil {
693+ return x.CreatedAt
694+ }
695+ return nil
696+}
697+
545 var File_ermite_storage_issue_proto protoreflect.FileDescriptor698 var File_ermite_storage_issue_proto protoreflect.FileDescriptor
546 699
547 const file_ermite_storage_issue_proto_rawDesc = "" +700 const file_ermite_storage_issue_proto_rawDesc = "" +
@@ -583,13 +736,29 @@ const file_ermite_storage_issue_proto_rawDesc = "" +
583 "\bIssueBan\x120\n" +736 "\bIssueBan\x120\n" +
584 "\x05until\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x05until\"\x1c\n" +737 "\x05until\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\x05until\"\x1c\n" +
585 "\bIssueKey\x12\x10\n" +738 "\bIssueKey\x12\x10\n" +
586- "\x03key\x18\x01 \x01(\fR\x03key*p\n" +739+ "\x03key\x18\x01 \x01(\fR\x03key\"\xe0\x01\n" +
740+ "\n" +
741+ "IssueEvent\x12\x10\n" +
742+ "\x03seq\x18\x01 \x01(\x03R\x03seq\x122\n" +
743+ "\x04kind\x18\x02 \x01(\x0e2\x1e.ermite.storage.IssueEventKindR\x04kind\x12#\n" +
744+ "\rmerge_request\x18\x03 \x01(\x03R\fmergeRequest\x12\x16\n" +
745+ "\x06closes\x18\x04 \x01(\bR\x06closes\x12\x14\n" +
746+ "\x05actor\x18\x05 \x01(\tR\x05actor\x129\n" +
747+ "\n" +
748+ "created_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt*\x8a\x01\n" +
587 "\n" +749 "\n" +
588 "IssueState\x12\x1b\n" +750 "IssueState\x12\x1b\n" +
589 "\x17ISSUE_STATE_UNSPECIFIED\x10\x00\x12\x14\n" +751 "\x17ISSUE_STATE_UNSPECIFIED\x10\x00\x12\x14\n" +
590 "\x10ISSUE_STATE_OPEN\x10\x01\x12\x16\n" +752 "\x10ISSUE_STATE_OPEN\x10\x01\x12\x16\n" +
591 "\x12ISSUE_STATE_CLOSED\x10\x02\x12\x17\n" +753 "\x12ISSUE_STATE_CLOSED\x10\x02\x12\x17\n" +
592- "\x13ISSUE_STATE_PENDING\x10\x03B/Z-bnema.dev/ermite/gen/ermite/storage;storagepbb\x06proto3"754+ "\x13ISSUE_STATE_PENDING\x10\x03\x12\x18\n" +
755+ "\x14ISSUE_STATE_RESOLVED\x10\x04*\xae\x01\n" +
756+ "\x0eIssueEventKind\x12 \n" +
757+ "\x1cISSUE_EVENT_KIND_UNSPECIFIED\x10\x00\x12\x1f\n" +
758+ "\x1bISSUE_EVENT_KIND_REFERENCED\x10\x01\x12\x1b\n" +
759+ "\x17ISSUE_EVENT_KIND_CLOSED\x10\x02\x12\x1d\n" +
760+ "\x19ISSUE_EVENT_KIND_REOPENED\x10\x03\x12\x1d\n" +
761+ "\x19ISSUE_EVENT_KIND_RESOLVED\x10\x04B/Z-bnema.dev/ermite/gen/ermite/storage;storagepbb\x06proto3"
593 762
594 var (763 var (
595 file_ermite_storage_issue_proto_rawDescOnce sync.Once764 file_ermite_storage_issue_proto_rawDescOnce sync.Once
@@ -603,31 +772,35 @@ func file_ermite_storage_issue_proto_rawDescGZIP() []byte {
603 return file_ermite_storage_issue_proto_rawDescData772 return file_ermite_storage_issue_proto_rawDescData
604 }773 }
605 774
606-var file_ermite_storage_issue_proto_enumTypes = make([]protoimpl.EnumInfo, 1)775+var file_ermite_storage_issue_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
607-var file_ermite_storage_issue_proto_msgTypes = make([]protoimpl.MessageInfo, 7)776+var file_ermite_storage_issue_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
608 var file_ermite_storage_issue_proto_goTypes = []any{777 var file_ermite_storage_issue_proto_goTypes = []any{
609 (IssueState)(0), // 0: ermite.storage.IssueState778 (IssueState)(0), // 0: ermite.storage.IssueState
610- (*Issue)(nil), // 1: ermite.storage.Issue779+ (IssueEventKind)(0), // 1: ermite.storage.IssueEventKind
611- (*IssueLink)(nil), // 2: ermite.storage.IssueLink780+ (*Issue)(nil), // 2: ermite.storage.Issue
612- (*MergeRequestReferences)(nil), // 3: ermite.storage.MergeRequestReferences781+ (*IssueLink)(nil), // 3: ermite.storage.IssueLink
613- (*IssueComment)(nil), // 4: ermite.storage.IssueComment782+ (*MergeRequestReferences)(nil), // 4: ermite.storage.MergeRequestReferences
614- (*PendingIssue)(nil), // 5: ermite.storage.PendingIssue783+ (*IssueComment)(nil), // 5: ermite.storage.IssueComment
615- (*IssueBan)(nil), // 6: ermite.storage.IssueBan784+ (*PendingIssue)(nil), // 6: ermite.storage.PendingIssue
616- (*IssueKey)(nil), // 7: ermite.storage.IssueKey785+ (*IssueBan)(nil), // 7: ermite.storage.IssueBan
617- (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp786+ (*IssueKey)(nil), // 8: ermite.storage.IssueKey
787+ (*IssueEvent)(nil), // 9: ermite.storage.IssueEvent
788+ (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
618 }789 }
619 var file_ermite_storage_issue_proto_depIdxs = []int32{790 var file_ermite_storage_issue_proto_depIdxs = []int32{
620- 0, // 0: ermite.storage.Issue.state:type_name -> ermite.storage.IssueState791+ 0, // 0: ermite.storage.Issue.state:type_name -> ermite.storage.IssueState
621- 8, // 1: ermite.storage.Issue.created_at:type_name -> google.protobuf.Timestamp792+ 10, // 1: ermite.storage.Issue.created_at:type_name -> google.protobuf.Timestamp
622- 8, // 2: ermite.storage.Issue.updated_at:type_name -> google.protobuf.Timestamp793+ 10, // 2: ermite.storage.Issue.updated_at:type_name -> google.protobuf.Timestamp
623- 8, // 3: ermite.storage.IssueComment.created_at:type_name -> google.protobuf.Timestamp794+ 10, // 3: ermite.storage.IssueComment.created_at:type_name -> google.protobuf.Timestamp
624- 8, // 4: ermite.storage.PendingIssue.created_at:type_name -> google.protobuf.Timestamp795+ 10, // 4: ermite.storage.PendingIssue.created_at:type_name -> google.protobuf.Timestamp
625- 8, // 5: ermite.storage.IssueBan.until:type_name -> google.protobuf.Timestamp796+ 10, // 5: ermite.storage.IssueBan.until:type_name -> google.protobuf.Timestamp
626- 6, // [6:6] is the sub-list for method output_type797+ 1, // 6: ermite.storage.IssueEvent.kind:type_name -> ermite.storage.IssueEventKind
627- 6, // [6:6] is the sub-list for method input_type798+ 10, // 7: ermite.storage.IssueEvent.created_at:type_name -> google.protobuf.Timestamp
628- 6, // [6:6] is the sub-list for extension type_name799+ 8, // [8:8] is the sub-list for method output_type
629- 6, // [6:6] is the sub-list for extension extendee800+ 8, // [8:8] is the sub-list for method input_type
630- 0, // [0:6] is the sub-list for field type_name801+ 8, // [8:8] is the sub-list for extension type_name
802+ 8, // [8:8] is the sub-list for extension extendee
803+ 0, // [0:8] is the sub-list for field type_name
631 }804 }
632 805
633 func init() { file_ermite_storage_issue_proto_init() }806 func init() { file_ermite_storage_issue_proto_init() }
@@ -640,8 +813,8 @@ func file_ermite_storage_issue_proto_init() {
640 File: protoimpl.DescBuilder{813 File: protoimpl.DescBuilder{
641 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),814 GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
642 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_storage_issue_proto_rawDesc), len(file_ermite_storage_issue_proto_rawDesc)),815 RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_storage_issue_proto_rawDesc), len(file_ermite_storage_issue_proto_rawDesc)),
643- NumEnums: 1,816+ NumEnums: 2,
644- NumMessages: 7,817+ NumMessages: 8,
645 NumExtensions: 0,818 NumExtensions: 0,
646 NumServices: 0,819 NumServices: 0,
647 },820 },
M internal/adapters/kvstore/issue_links.go +6 -1
@@ -39,7 +39,7 @@ func issueRefKey(repo domain.RepoName, issue, mr int64) []byte {
3939 }
4040
4141 // ReplaceLinks atomically replaces both directions of the index.
42-func (l *IssueLinks) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64) (added, removed []domain.IssueLink, err error) {
42+func (l *IssueLinks) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64, ref ports.ReferenceEvent) (added, removed []domain.IssueLink, err error) {
4343 key := mrRefKey(repo, mr)
4444 item, exists := l.db.Get(ctx, key)
4545 var old storagepb.MergeRequestReferences
@@ -149,3 +149,8 @@ func toSet(numbers []int64) map[int64]struct{} {
149149 }
150150 return set
151151 }
152+
153+// EventsOfMergeRequest returns MR issue history. Persistence is implemented in Phase A.
154+func (l *IssueLinks) EventsOfMergeRequest(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error) {
155+ return nil, nil
156+}
@@ -39,7 +39,7 @@ func issueRefKey(repo domain.RepoName, issue, mr int64) []byte {
39 }39 }
40 40
41 // ReplaceLinks atomically replaces both directions of the index.41 // ReplaceLinks atomically replaces both directions of the index.
42-func (l *IssueLinks) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64) (added, removed []domain.IssueLink, err error) {42+func (l *IssueLinks) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64, ref ports.ReferenceEvent) (added, removed []domain.IssueLink, err error) {
43 key := mrRefKey(repo, mr)43 key := mrRefKey(repo, mr)
44 item, exists := l.db.Get(ctx, key)44 item, exists := l.db.Get(ctx, key)
45 var old storagepb.MergeRequestReferences45 var old storagepb.MergeRequestReferences
@@ -149,3 +149,8 @@ func toSet(numbers []int64) map[int64]struct{} {
149 }149 }
150 return set150 return set
151 }151 }
152+
153+// EventsOfMergeRequest returns MR issue history. Persistence is implemented in Phase A.
154+func (l *IssueLinks) EventsOfMergeRequest(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error) {
155+ return nil, nil
156+}
M internal/adapters/kvstore/issue_links_test.go +7 -6
@@ -3,6 +3,7 @@ package kvstore_test
33
44 import (
55 "bnema.dev/ermite/internal/domain"
6+ "bnema.dev/ermite/internal/ports"
67 "context"
78 "github.com/stretchr/testify/assert"
89 "github.com/stretchr/testify/require"
@@ -13,30 +14,30 @@ func TestIssueLinksReplace(t *testing.T) {
1314 ctx := context.Background()
1415 s, _, repo := repoWithMRs(t)
1516 links := s.IssueLinks()
16- added, removed, err := links.ReplaceLinks(ctx, repo, 9, []int64{3, 2, 3}, []int64{2})
17+ added, removed, err := links.ReplaceLinks(ctx, repo, 9, []int64{3, 2, 3}, []int64{2}, ports.ReferenceEvent{})
1718 require.NoError(t, err)
1819 assert.Equal(t, []domain.IssueLink{{Issue: 2, MergeRequest: 9, Closes: true}, {Issue: 3, MergeRequest: 9}}, added)
1920 assert.Empty(t, removed)
2021 got, err := links.LinksOfMergeRequest(ctx, repo, 9)
2122 require.NoError(t, err)
2223 assert.Equal(t, added, got)
23- added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{2, 3}, []int64{2})
24+ added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{2, 3}, []int64{2}, ports.ReferenceEvent{})
2425 require.NoError(t, err)
2526 assert.Empty(t, added)
2627 assert.Empty(t, removed)
27- added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3, 4}, []int64{3})
28+ added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3, 4}, []int64{3}, ports.ReferenceEvent{})
2829 require.NoError(t, err)
2930 assert.Equal(t, []domain.IssueLink{{Issue: 3, MergeRequest: 9, Closes: true}, {Issue: 4, MergeRequest: 9}}, added)
3031 assert.Equal(t, []domain.IssueLink{{Issue: 2, MergeRequest: 9, Closes: true}}, removed)
3132 got, err = links.LinksOfIssue(ctx, repo, 2, 100)
3233 require.NoError(t, err)
3334 assert.Empty(t, got)
34- added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3}, nil)
35+ added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3}, nil, ports.ReferenceEvent{})
3536 require.NoError(t, err)
3637 assert.Empty(t, added)
3738 assert.Equal(t, []domain.IssueLink{{Issue: 3, MergeRequest: 9, Closes: true}, {Issue: 4, MergeRequest: 9}}, removed)
3839 for _, mr := range []int64{12, 1, 5} {
39- _, _, err = links.ReplaceLinks(ctx, repo, mr, []int64{3}, nil)
40+ _, _, err = links.ReplaceLinks(ctx, repo, mr, []int64{3}, nil, ports.ReferenceEvent{})
4041 require.NoError(t, err)
4142 }
4243 got, err = links.LinksOfIssue(ctx, repo, 3, 2)
@@ -45,7 +46,7 @@ func TestIssueLinksReplace(t *testing.T) {
4546 require.NoError(t, s.Delete(ctx, repo))
4647 assert.Zero(t, countKeys(t, s, "isref/"+repo.String()+"/"))
4748 assert.Zero(t, countKeys(t, s, "mrref/"+repo.String()+"/"))
48- _, _, err = links.ReplaceLinks(ctx, repo, 9, []int64{2}, nil)
49+ _, _, err = links.ReplaceLinks(ctx, repo, 9, []int64{2}, nil, ports.ReferenceEvent{})
4950 assert.ErrorIs(t, err, domain.ErrNotFound)
5051 }
5152
@@ -3,6 +3,7 @@ package kvstore_test
3 3
4 import (4 import (
5 "bnema.dev/ermite/internal/domain"5 "bnema.dev/ermite/internal/domain"
6+ "bnema.dev/ermite/internal/ports"
6 "context"7 "context"
7 "github.com/stretchr/testify/assert"8 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
@@ -13,30 +14,30 @@ func TestIssueLinksReplace(t *testing.T) {
13 ctx := context.Background()14 ctx := context.Background()
14 s, _, repo := repoWithMRs(t)15 s, _, repo := repoWithMRs(t)
15 links := s.IssueLinks()16 links := s.IssueLinks()
16- added, removed, err := links.ReplaceLinks(ctx, repo, 9, []int64{3, 2, 3}, []int64{2})17+ added, removed, err := links.ReplaceLinks(ctx, repo, 9, []int64{3, 2, 3}, []int64{2}, ports.ReferenceEvent{})
17 require.NoError(t, err)18 require.NoError(t, err)
18 assert.Equal(t, []domain.IssueLink{{Issue: 2, MergeRequest: 9, Closes: true}, {Issue: 3, MergeRequest: 9}}, added)19 assert.Equal(t, []domain.IssueLink{{Issue: 2, MergeRequest: 9, Closes: true}, {Issue: 3, MergeRequest: 9}}, added)
19 assert.Empty(t, removed)20 assert.Empty(t, removed)
20 got, err := links.LinksOfMergeRequest(ctx, repo, 9)21 got, err := links.LinksOfMergeRequest(ctx, repo, 9)
21 require.NoError(t, err)22 require.NoError(t, err)
22 assert.Equal(t, added, got)23 assert.Equal(t, added, got)
23- added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{2, 3}, []int64{2})24+ added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{2, 3}, []int64{2}, ports.ReferenceEvent{})
24 require.NoError(t, err)25 require.NoError(t, err)
25 assert.Empty(t, added)26 assert.Empty(t, added)
26 assert.Empty(t, removed)27 assert.Empty(t, removed)
27- added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3, 4}, []int64{3})28+ added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3, 4}, []int64{3}, ports.ReferenceEvent{})
28 require.NoError(t, err)29 require.NoError(t, err)
29 assert.Equal(t, []domain.IssueLink{{Issue: 3, MergeRequest: 9, Closes: true}, {Issue: 4, MergeRequest: 9}}, added)30 assert.Equal(t, []domain.IssueLink{{Issue: 3, MergeRequest: 9, Closes: true}, {Issue: 4, MergeRequest: 9}}, added)
30 assert.Equal(t, []domain.IssueLink{{Issue: 2, MergeRequest: 9, Closes: true}}, removed)31 assert.Equal(t, []domain.IssueLink{{Issue: 2, MergeRequest: 9, Closes: true}}, removed)
31 got, err = links.LinksOfIssue(ctx, repo, 2, 100)32 got, err = links.LinksOfIssue(ctx, repo, 2, 100)
32 require.NoError(t, err)33 require.NoError(t, err)
33 assert.Empty(t, got)34 assert.Empty(t, got)
34- added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3}, nil)35+ added, removed, err = links.ReplaceLinks(ctx, repo, 9, []int64{3}, nil, ports.ReferenceEvent{})
35 require.NoError(t, err)36 require.NoError(t, err)
36 assert.Empty(t, added)37 assert.Empty(t, added)
37 assert.Equal(t, []domain.IssueLink{{Issue: 3, MergeRequest: 9, Closes: true}, {Issue: 4, MergeRequest: 9}}, removed)38 assert.Equal(t, []domain.IssueLink{{Issue: 3, MergeRequest: 9, Closes: true}, {Issue: 4, MergeRequest: 9}}, removed)
38 for _, mr := range []int64{12, 1, 5} {39 for _, mr := range []int64{12, 1, 5} {
39- _, _, err = links.ReplaceLinks(ctx, repo, mr, []int64{3}, nil)40+ _, _, err = links.ReplaceLinks(ctx, repo, mr, []int64{3}, nil, ports.ReferenceEvent{})
40 require.NoError(t, err)41 require.NoError(t, err)
41 }42 }
42 got, err = links.LinksOfIssue(ctx, repo, 3, 2)43 got, err = links.LinksOfIssue(ctx, repo, 3, 2)
@@ -45,7 +46,7 @@ func TestIssueLinksReplace(t *testing.T) {
45 require.NoError(t, s.Delete(ctx, repo))46 require.NoError(t, s.Delete(ctx, repo))
46 assert.Zero(t, countKeys(t, s, "isref/"+repo.String()+"/"))47 assert.Zero(t, countKeys(t, s, "isref/"+repo.String()+"/"))
47 assert.Zero(t, countKeys(t, s, "mrref/"+repo.String()+"/"))48 assert.Zero(t, countKeys(t, s, "mrref/"+repo.String()+"/"))
48- _, _, err = links.ReplaceLinks(ctx, repo, 9, []int64{2}, nil)49+ _, _, err = links.ReplaceLinks(ctx, repo, 9, []int64{2}, nil, ports.ReferenceEvent{})
49 assert.ErrorIs(t, err, domain.ErrNotFound)50 assert.ErrorIs(t, err, domain.ErrNotFound)
50 }51 }
51 52
M internal/adapters/kvstore/issues.go +10 -1
@@ -221,7 +221,7 @@ func (i *Issues) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
221221 // Update replaces the issue with a version precondition and moves its index key
222222 // in the same batch. The repository must exist (see guardRepoKey), so that a
223223 // concurrent delete of the repository leaves no index key behind.
224-func (i *Issues) Update(ctx context.Context, issue domain.Issue) error {
224+func (i *Issues) Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error {
225225 val, err := encodeIssue(issue)
226226 if err != nil {
227227 return err
@@ -429,3 +429,12 @@ func decodeIssuesMode(m storagepb.IssuesMode) domain.IssuesMode {
429429 }
430430 return domain.IssuesOff
431431 }
432+
433+// MaxIssueEvents caps the append-only history of each issue. Further events
434+// are silently dropped without preventing the associated write.
435+const MaxIssueEvents = 500
436+
437+// ListEvents returns issue history. Persistence is implemented in Phase A.
438+func (i *Issues) ListEvents(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error) {
439+ return nil, nil
440+}
@@ -221,7 +221,7 @@ func (i *Issues) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
221 // Update replaces the issue with a version precondition and moves its index key221 // Update replaces the issue with a version precondition and moves its index key
222 // in the same batch. The repository must exist (see guardRepoKey), so that a222 // in the same batch. The repository must exist (see guardRepoKey), so that a
223 // concurrent delete of the repository leaves no index key behind.223 // concurrent delete of the repository leaves no index key behind.
224-func (i *Issues) Update(ctx context.Context, issue domain.Issue) error {224+func (i *Issues) Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error {
225 val, err := encodeIssue(issue)225 val, err := encodeIssue(issue)
226 if err != nil {226 if err != nil {
227 return err227 return err
@@ -429,3 +429,12 @@ func decodeIssuesMode(m storagepb.IssuesMode) domain.IssuesMode {
429 }429 }
430 return domain.IssuesOff430 return domain.IssuesOff
431 }431 }
432+
433+// MaxIssueEvents caps the append-only history of each issue. Further events
434+// are silently dropped without preventing the associated write.
435+const MaxIssueEvents = 500
436+
437+// ListEvents returns issue history. Persistence is implemented in Phase A.
438+func (i *Issues) ListEvents(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error) {
439+ return nil, nil
440+}
M internal/adapters/rpc/issue.go +5 -0
@@ -270,3 +270,8 @@ func (s *issueService) RejectIssue(ctx context.Context, req *connect.Request[pb.
270270 }
271271 return connect.NewResponse(&pb.RejectIssueResponse{}), nil
272272 }
273+
274+// ResolveIssue is reserved for Phase A; the RPC is not implemented yet.
275+func (s *issueService) ResolveIssue(ctx context.Context, req *connect.Request[pb.ResolveIssueRequest]) (*connect.Response[pb.ResolveIssueResponse], error) {
276+ return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("ResolveIssue is not implemented"))
277+}
@@ -270,3 +270,8 @@ func (s *issueService) RejectIssue(ctx context.Context, req *connect.Request[pb.
270 }270 }
271 return connect.NewResponse(&pb.RejectIssueResponse{}), nil271 return connect.NewResponse(&pb.RejectIssueResponse{}), nil
272 }272 }
273+
274+// ResolveIssue is reserved for Phase A; the RPC is not implemented yet.
275+func (s *issueService) ResolveIssue(ctx context.Context, req *connect.Request[pb.ResolveIssueRequest]) (*connect.Response[pb.ResolveIssueResponse], error) {
276+ return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("ResolveIssue is not implemented"))
277+}
M internal/domain/event.go +11 -0
@@ -237,6 +237,17 @@ type IssueClosed struct {
237237 // EventKind implements Event.
238238 func (IssueClosed) EventKind() string { return "issue.closed" }
239239
240+// IssueResolved is published when an issue was resolved.
241+type IssueResolved struct {
242+ Issue Issue
243+ At time.Time
244+ Visibility Visibility
245+ CreatedAt time.Time
246+}
247+
248+// EventKind implements Event.
249+func (IssueResolved) EventKind() string { return "issue.resolved" }
250+
240251 // IssueReopened is published when a closed issue was reopened.
241252 type IssueReopened struct {
242253 Issue Issue
@@ -237,6 +237,17 @@ type IssueClosed struct {
237 // EventKind implements Event.237 // EventKind implements Event.
238 func (IssueClosed) EventKind() string { return "issue.closed" }238 func (IssueClosed) EventKind() string { return "issue.closed" }
239 239
240+// IssueResolved is published when an issue was resolved.
241+type IssueResolved struct {
242+ Issue Issue
243+ At time.Time
244+ Visibility Visibility
245+ CreatedAt time.Time
246+}
247+
248+// EventKind implements Event.
249+func (IssueResolved) EventKind() string { return "issue.resolved" }
250+
240 // IssueReopened is published when a closed issue was reopened.251 // IssueReopened is published when a closed issue was reopened.
241 type IssueReopened struct {252 type IssueReopened struct {
242 Issue Issue253 Issue Issue
M internal/domain/event_test.go +6 -0
@@ -59,3 +59,9 @@ func TestAutoMergeEventKinds(t *testing.T) {
5959 assert.Equal(t, "mr.auto_merge_cancelled", domain.MergeRequestAutoMergeCancelled{}.EventKind())
6060 assert.Equal(t, "mr.merged", domain.MergeRequestMerged{Auto: true}.EventKind())
6161 }
62+
63+func TestIssueResolvedEventKind(t *testing.T) {
64+ t.Parallel()
65+ var ev domain.Event = domain.IssueResolved{}
66+ assert.Equal(t, "issue.resolved", ev.EventKind())
67+}
@@ -59,3 +59,9 @@ func TestAutoMergeEventKinds(t *testing.T) {
59 assert.Equal(t, "mr.auto_merge_cancelled", domain.MergeRequestAutoMergeCancelled{}.EventKind())59 assert.Equal(t, "mr.auto_merge_cancelled", domain.MergeRequestAutoMergeCancelled{}.EventKind())
60 assert.Equal(t, "mr.merged", domain.MergeRequestMerged{Auto: true}.EventKind())60 assert.Equal(t, "mr.merged", domain.MergeRequestMerged{Auto: true}.EventKind())
61 }61 }
62+
63+func TestIssueResolvedEventKind(t *testing.T) {
64+ t.Parallel()
65+ var ev domain.Event = domain.IssueResolved{}
66+ assert.Equal(t, "issue.resolved", ev.EventKind())
67+}
M internal/domain/issue.go +29 -2
@@ -51,8 +51,10 @@ type IssueState string
5151 const (
5252 // IssueStateOpen is being worked on.
5353 IssueStateOpen IssueState = "open"
54- // IssueStateClosed is done with; it can be reopened.
54+ // IssueStateClosed is closed without being resolved; it can be reopened.
5555 IssueStateClosed IssueState = "closed"
56+ // IssueStateResolved is resolved; it can be reopened.
57+ IssueStateResolved IssueState = "resolved"
5658 )
5759
5860 // Issue is a problem or a request about a repository. It shares its number
@@ -73,7 +75,7 @@ type Issue struct {
7375 // External is true for an issue approved from the public form: its text
7476 // comes from an anonymous visitor and is rendered as untrusted.
7577 External bool
76- // ClosedByMergeRequest is zero unless a merge closed the issue.
78+ // ClosedByMergeRequest is set only while resolved by a merge.
7779 ClosedByMergeRequest int64
7880 // Version is set by the store on read: Update fails with ErrConflict when
7981 // the stored issue changed since. Opaque to everyone else.
@@ -112,3 +114,28 @@ const (
112114 // NumberMergeRequest: the number names a merge request.
113115 NumberMergeRequest
114116 )
117+
118+// IsDone reports whether the issue is closed or resolved.
119+func (i Issue) IsDone() bool { return i.State == IssueStateClosed || i.State == IssueStateResolved }
120+
121+// IssueEventKind describes an entry in an issue's append-only history.
122+type IssueEventKind string
123+
124+// Issue event kinds.
125+const (
126+ IssueEventReferenced IssueEventKind = "referenced"
127+ IssueEventClosed IssueEventKind = "closed"
128+ IssueEventReopened IssueEventKind = "reopened"
129+ IssueEventResolved IssueEventKind = "resolved"
130+)
131+
132+// IssueEvent is a timestamped history entry with a per-issue sequence ID.
133+type IssueEvent struct {
134+ ID int64
135+ Kind IssueEventKind
136+ Issue int64
137+ MergeRequest int64
138+ Closes bool
139+ Actor string
140+ CreatedAt time.Time
141+}
@@ -51,8 +51,10 @@ type IssueState string
51 const (51 const (
52 // IssueStateOpen is being worked on.52 // IssueStateOpen is being worked on.
53 IssueStateOpen IssueState = "open"53 IssueStateOpen IssueState = "open"
54- // IssueStateClosed is done with; it can be reopened.54+ // IssueStateClosed is closed without being resolved; it can be reopened.
55 IssueStateClosed IssueState = "closed"55 IssueStateClosed IssueState = "closed"
56+ // IssueStateResolved is resolved; it can be reopened.
57+ IssueStateResolved IssueState = "resolved"
56 )58 )
57 59
58 // Issue is a problem or a request about a repository. It shares its number60 // Issue is a problem or a request about a repository. It shares its number
@@ -73,7 +75,7 @@ type Issue struct {
73 // External is true for an issue approved from the public form: its text75 // External is true for an issue approved from the public form: its text
74 // comes from an anonymous visitor and is rendered as untrusted.76 // comes from an anonymous visitor and is rendered as untrusted.
75 External bool77 External bool
76- // ClosedByMergeRequest is zero unless a merge closed the issue.78+ // ClosedByMergeRequest is set only while resolved by a merge.
77 ClosedByMergeRequest int6479 ClosedByMergeRequest int64
78 // Version is set by the store on read: Update fails with ErrConflict when80 // Version is set by the store on read: Update fails with ErrConflict when
79 // the stored issue changed since. Opaque to everyone else.81 // the stored issue changed since. Opaque to everyone else.
@@ -112,3 +114,28 @@ const (
112 // NumberMergeRequest: the number names a merge request.114 // NumberMergeRequest: the number names a merge request.
113 NumberMergeRequest115 NumberMergeRequest
114 )116 )
117+
118+// IsDone reports whether the issue is closed or resolved.
119+func (i Issue) IsDone() bool { return i.State == IssueStateClosed || i.State == IssueStateResolved }
120+
121+// IssueEventKind describes an entry in an issue's append-only history.
122+type IssueEventKind string
123+
124+// Issue event kinds.
125+const (
126+ IssueEventReferenced IssueEventKind = "referenced"
127+ IssueEventClosed IssueEventKind = "closed"
128+ IssueEventReopened IssueEventKind = "reopened"
129+ IssueEventResolved IssueEventKind = "resolved"
130+)
131+
132+// IssueEvent is a timestamped history entry with a per-issue sequence ID.
133+type IssueEvent struct {
134+ ID int64
135+ Kind IssueEventKind
136+ Issue int64
137+ MergeRequest int64
138+ Closes bool
139+ Actor string
140+ CreatedAt time.Time
141+}
M internal/domain/issue_test.go +19 -0
@@ -136,3 +136,22 @@ func TestIssueEventKinds(t *testing.T) {
136136 assert.Equal(t, "issue.reopened", domain.IssueReopened{}.EventKind())
137137 assert.Equal(t, "issue.commented", domain.IssueCommented{}.EventKind())
138138 }
139+
140+func TestIssueIsDone(t *testing.T) {
141+ t.Parallel()
142+ for _, state := range []domain.IssueState{"", "unknown", domain.IssueStateOpen, domain.IssueStateClosed, domain.IssueStateResolved} {
143+ t.Run(string(state), func(t *testing.T) {
144+ issue := domain.Issue{State: state}
145+ assert.Equal(t, state == domain.IssueStateClosed || state == domain.IssueStateResolved, issue.IsDone())
146+ assert.Equal(t, state == domain.IssueStateOpen, issue.IsOpen())
147+ })
148+ }
149+}
150+
151+func TestIssueHistoryEventKinds(t *testing.T) {
152+ t.Parallel()
153+ assert.Equal(t, "referenced", string(domain.IssueEventReferenced))
154+ assert.Equal(t, "closed", string(domain.IssueEventClosed))
155+ assert.Equal(t, "reopened", string(domain.IssueEventReopened))
156+ assert.Equal(t, "resolved", string(domain.IssueEventResolved))
157+}
@@ -136,3 +136,22 @@ func TestIssueEventKinds(t *testing.T) {
136 assert.Equal(t, "issue.reopened", domain.IssueReopened{}.EventKind())136 assert.Equal(t, "issue.reopened", domain.IssueReopened{}.EventKind())
137 assert.Equal(t, "issue.commented", domain.IssueCommented{}.EventKind())137 assert.Equal(t, "issue.commented", domain.IssueCommented{}.EventKind())
138 }138 }
139+
140+func TestIssueIsDone(t *testing.T) {
141+ t.Parallel()
142+ for _, state := range []domain.IssueState{"", "unknown", domain.IssueStateOpen, domain.IssueStateClosed, domain.IssueStateResolved} {
143+ t.Run(string(state), func(t *testing.T) {
144+ issue := domain.Issue{State: state}
145+ assert.Equal(t, state == domain.IssueStateClosed || state == domain.IssueStateResolved, issue.IsDone())
146+ assert.Equal(t, state == domain.IssueStateOpen, issue.IsOpen())
147+ })
148+ }
149+}
150+
151+func TestIssueHistoryEventKinds(t *testing.T) {
152+ t.Parallel()
153+ assert.Equal(t, "referenced", string(domain.IssueEventReferenced))
154+ assert.Equal(t, "closed", string(domain.IssueEventClosed))
155+ assert.Equal(t, "reopened", string(domain.IssueEventReopened))
156+ assert.Equal(t, "resolved", string(domain.IssueEventResolved))
157+}
M internal/ports/dto.go +26 -0
@@ -261,6 +261,8 @@ type IssueReference struct {
261261
262262 // IssueDetail is an issue with its comments and referencing merge requests.
263263 type IssueDetail struct {
264+ // Events are oldest first.
265+ Events []IssueEventDetail
264266 // References are ascending by number, at most 100.
265267 References []IssueReference
266268 Issue domain.Issue
@@ -284,6 +286,10 @@ type PushSummary struct {
284286
285287 // MergeRequestDetail is a merge request with its review data.
286288 type MergeRequestDetail struct {
289+ // Issues are current links ascending by number, at most 100; empty when issues are off.
290+ Issues []MergeRequestIssue
291+ // IssueEvents are append-only history, including links since removed.
292+ IssueEvents []IssueEventDetail
287293 MergeRequest domain.MergeRequest
288294 // NewCommits is the unread prefix of Commits; zero if the read head is absent.
289295 NewCommits int
@@ -439,3 +445,23 @@ const (
439445 // MaxPushSummaryCommits bounds the commits counted in a push summary.
440446 MaxPushSummaryCommits = 100
441447 )
448+
449+// ReferenceEvent says who and when a reference was recorded.
450+type ReferenceEvent struct {
451+ Actor string
452+ At time.Time
453+}
454+
455+// IssueEventDetail includes the current MR title, empty if the MR is gone.
456+type IssueEventDetail struct {
457+ Event domain.IssueEvent
458+ MergeRequestTitle domain.MRTitle
459+}
460+
461+// MergeRequestIssue is a current issue link on a merge request.
462+type MergeRequestIssue struct {
463+ Number int64
464+ Title domain.IssueTitle
465+ State domain.IssueState
466+ Closes bool
467+}
@@ -261,6 +261,8 @@ type IssueReference struct {
261 261
262 // IssueDetail is an issue with its comments and referencing merge requests.262 // IssueDetail is an issue with its comments and referencing merge requests.
263 type IssueDetail struct {263 type IssueDetail struct {
264+ // Events are oldest first.
265+ Events []IssueEventDetail
264 // References are ascending by number, at most 100.266 // References are ascending by number, at most 100.
265 References []IssueReference267 References []IssueReference
266 Issue domain.Issue268 Issue domain.Issue
@@ -284,6 +286,10 @@ type PushSummary struct {
284 286
285 // MergeRequestDetail is a merge request with its review data.287 // MergeRequestDetail is a merge request with its review data.
286 type MergeRequestDetail struct {288 type MergeRequestDetail struct {
289+ // Issues are current links ascending by number, at most 100; empty when issues are off.
290+ Issues []MergeRequestIssue
291+ // IssueEvents are append-only history, including links since removed.
292+ IssueEvents []IssueEventDetail
287 MergeRequest domain.MergeRequest293 MergeRequest domain.MergeRequest
288 // NewCommits is the unread prefix of Commits; zero if the read head is absent.294 // NewCommits is the unread prefix of Commits; zero if the read head is absent.
289 NewCommits int295 NewCommits int
@@ -439,3 +445,23 @@ const (
439 // MaxPushSummaryCommits bounds the commits counted in a push summary.445 // MaxPushSummaryCommits bounds the commits counted in a push summary.
440 MaxPushSummaryCommits = 100446 MaxPushSummaryCommits = 100
441 )447 )
448+
449+// ReferenceEvent says who and when a reference was recorded.
450+type ReferenceEvent struct {
451+ Actor string
452+ At time.Time
453+}
454+
455+// IssueEventDetail includes the current MR title, empty if the MR is gone.
456+type IssueEventDetail struct {
457+ Event domain.IssueEvent
458+ MergeRequestTitle domain.MRTitle
459+}
460+
461+// MergeRequestIssue is a current issue link on a merge request.
462+type MergeRequestIssue struct {
463+ Number int64
464+ Title domain.IssueTitle
465+ State domain.IssueState
466+ Closes bool
467+}
M internal/ports/mocks/IssueLinkStore.go +95 -14
@@ -8,6 +8,7 @@ import (
88 "context"
99
1010 "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
1112 mock "github.com/stretchr/testify/mock"
1213 )
1314
@@ -47,6 +48,80 @@ func (_m *IssueLinkStore) EXPECT() *IssueLinkStore_Expecter {
4748 return &IssueLinkStore_Expecter{mock: &_m.Mock}
4849 }
4950
51+// EventsOfMergeRequest provides a mock function for the type IssueLinkStore
52+func (_mock *IssueLinkStore) EventsOfMergeRequest(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error) {
53+ ret := _mock.Called(ctx, repo, mr)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for EventsOfMergeRequest")
57+ }
58+
59+ var r0 []domain.IssueEvent
60+ var r1 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.IssueEvent, error)); ok {
62+ return returnFunc(ctx, repo, mr)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.IssueEvent); ok {
65+ r0 = returnFunc(ctx, repo, mr)
66+ } else {
67+ if ret.Get(0) != nil {
68+ r0 = ret.Get(0).([]domain.IssueEvent)
69+ }
70+ }
71+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
72+ r1 = returnFunc(ctx, repo, mr)
73+ } else {
74+ r1 = ret.Error(1)
75+ }
76+ return r0, r1
77+}
78+
79+// IssueLinkStore_EventsOfMergeRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EventsOfMergeRequest'
80+type IssueLinkStore_EventsOfMergeRequest_Call struct {
81+ *mock.Call
82+}
83+
84+// EventsOfMergeRequest is a helper method to define mock.On call
85+// - ctx context.Context
86+// - repo domain.RepoName
87+// - mr int64
88+func (_e *IssueLinkStore_Expecter) EventsOfMergeRequest(ctx any, repo any, mr any) *IssueLinkStore_EventsOfMergeRequest_Call {
89+ return &IssueLinkStore_EventsOfMergeRequest_Call{Call: _e.mock.On("EventsOfMergeRequest", ctx, repo, mr)}
90+}
91+
92+func (_c *IssueLinkStore_EventsOfMergeRequest_Call) Run(run func(ctx context.Context, repo domain.RepoName, mr int64)) *IssueLinkStore_EventsOfMergeRequest_Call {
93+ _c.Call.Run(func(args mock.Arguments) {
94+ var arg0 context.Context
95+ if args[0] != nil {
96+ arg0 = args[0].(context.Context)
97+ }
98+ var arg1 domain.RepoName
99+ if args[1] != nil {
100+ arg1 = args[1].(domain.RepoName)
101+ }
102+ var arg2 int64
103+ if args[2] != nil {
104+ arg2 = args[2].(int64)
105+ }
106+ run(
107+ arg0,
108+ arg1,
109+ arg2,
110+ )
111+ })
112+ return _c
113+}
114+
115+func (_c *IssueLinkStore_EventsOfMergeRequest_Call) Return(issueEvents []domain.IssueEvent, err error) *IssueLinkStore_EventsOfMergeRequest_Call {
116+ _c.Call.Return(issueEvents, err)
117+ return _c
118+}
119+
120+func (_c *IssueLinkStore_EventsOfMergeRequest_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error)) *IssueLinkStore_EventsOfMergeRequest_Call {
121+ _c.Call.Return(run)
122+ return _c
123+}
124+
50125 // LinksOfIssue provides a mock function for the type IssueLinkStore
51126 func (_mock *IssueLinkStore) LinksOfIssue(ctx context.Context, repo domain.RepoName, issue int64, limit int) ([]domain.IssueLink, error) {
52127 ret := _mock.Called(ctx, repo, issue, limit)
@@ -202,8 +277,8 @@ func (_c *IssueLinkStore_LinksOfMergeRequest_Call) RunAndReturn(run func(ctx con
202277 }
203278
204279 // ReplaceLinks provides a mock function for the type IssueLinkStore
205-func (_mock *IssueLinkStore) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64) ([]domain.IssueLink, []domain.IssueLink, error) {
206- ret := _mock.Called(ctx, repo, mr, issues, closes)
280+func (_mock *IssueLinkStore) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64, ref ports.ReferenceEvent) ([]domain.IssueLink, []domain.IssueLink, error) {
281+ ret := _mock.Called(ctx, repo, mr, issues, closes, ref)
207282
208283 if len(ret) == 0 {
209284 panic("no return value specified for ReplaceLinks")
@@ -212,25 +287,25 @@ func (_mock *IssueLinkStore) ReplaceLinks(ctx context.Context, repo domain.RepoN
212287 var r0 []domain.IssueLink
213288 var r1 []domain.IssueLink
214289 var r2 error
215- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64) ([]domain.IssueLink, []domain.IssueLink, error)); ok {
216- return returnFunc(ctx, repo, mr, issues, closes)
290+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) ([]domain.IssueLink, []domain.IssueLink, error)); ok {
291+ return returnFunc(ctx, repo, mr, issues, closes, ref)
217292 }
218- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64) []domain.IssueLink); ok {
219- r0 = returnFunc(ctx, repo, mr, issues, closes)
293+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) []domain.IssueLink); ok {
294+ r0 = returnFunc(ctx, repo, mr, issues, closes, ref)
220295 } else {
221296 if ret.Get(0) != nil {
222297 r0 = ret.Get(0).([]domain.IssueLink)
223298 }
224299 }
225- if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, []int64, []int64) []domain.IssueLink); ok {
226- r1 = returnFunc(ctx, repo, mr, issues, closes)
300+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) []domain.IssueLink); ok {
301+ r1 = returnFunc(ctx, repo, mr, issues, closes, ref)
227302 } else {
228303 if ret.Get(1) != nil {
229304 r1 = ret.Get(1).([]domain.IssueLink)
230305 }
231306 }
232- if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, int64, []int64, []int64) error); ok {
233- r2 = returnFunc(ctx, repo, mr, issues, closes)
307+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) error); ok {
308+ r2 = returnFunc(ctx, repo, mr, issues, closes, ref)
234309 } else {
235310 r2 = ret.Error(2)
236311 }
@@ -248,11 +323,12 @@ type IssueLinkStore_ReplaceLinks_Call struct {
248323 // - mr int64
249324 // - issues []int64
250325 // - closes []int64
251-func (_e *IssueLinkStore_Expecter) ReplaceLinks(ctx any, repo any, mr any, issues any, closes any) *IssueLinkStore_ReplaceLinks_Call {
252- return &IssueLinkStore_ReplaceLinks_Call{Call: _e.mock.On("ReplaceLinks", ctx, repo, mr, issues, closes)}
326+// - ref ports.ReferenceEvent
327+func (_e *IssueLinkStore_Expecter) ReplaceLinks(ctx any, repo any, mr any, issues any, closes any, ref any) *IssueLinkStore_ReplaceLinks_Call {
328+ return &IssueLinkStore_ReplaceLinks_Call{Call: _e.mock.On("ReplaceLinks", ctx, repo, mr, issues, closes, ref)}
253329 }
254330
255-func (_c *IssueLinkStore_ReplaceLinks_Call) Run(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64)) *IssueLinkStore_ReplaceLinks_Call {
331+func (_c *IssueLinkStore_ReplaceLinks_Call) Run(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64, ref ports.ReferenceEvent)) *IssueLinkStore_ReplaceLinks_Call {
256332 _c.Call.Run(func(args mock.Arguments) {
257333 var arg0 context.Context
258334 if args[0] != nil {
@@ -274,12 +350,17 @@ func (_c *IssueLinkStore_ReplaceLinks_Call) Run(run func(ctx context.Context, re
274350 if args[4] != nil {
275351 arg4 = args[4].([]int64)
276352 }
353+ var arg5 ports.ReferenceEvent
354+ if args[5] != nil {
355+ arg5 = args[5].(ports.ReferenceEvent)
356+ }
277357 run(
278358 arg0,
279359 arg1,
280360 arg2,
281361 arg3,
282362 arg4,
363+ arg5,
283364 )
284365 })
285366 return _c
@@ -290,7 +371,7 @@ func (_c *IssueLinkStore_ReplaceLinks_Call) Return(added []domain.IssueLink, rem
290371 return _c
291372 }
292373
293-func (_c *IssueLinkStore_ReplaceLinks_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64) ([]domain.IssueLink, []domain.IssueLink, error)) *IssueLinkStore_ReplaceLinks_Call {
374+func (_c *IssueLinkStore_ReplaceLinks_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64, ref ports.ReferenceEvent) ([]domain.IssueLink, []domain.IssueLink, error)) *IssueLinkStore_ReplaceLinks_Call {
294375 _c.Call.Return(run)
295376 return _c
296377 }
@@ -8,6 +8,7 @@ import (
8 "context"8 "context"
9 9
10 "bnema.dev/ermite/internal/domain"10 "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
11 mock "github.com/stretchr/testify/mock"12 mock "github.com/stretchr/testify/mock"
12 )13 )
13 14
@@ -47,6 +48,80 @@ func (_m *IssueLinkStore) EXPECT() *IssueLinkStore_Expecter {
47 return &IssueLinkStore_Expecter{mock: &_m.Mock}48 return &IssueLinkStore_Expecter{mock: &_m.Mock}
48 }49 }
49 50
51+// EventsOfMergeRequest provides a mock function for the type IssueLinkStore
52+func (_mock *IssueLinkStore) EventsOfMergeRequest(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error) {
53+ ret := _mock.Called(ctx, repo, mr)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for EventsOfMergeRequest")
57+ }
58+
59+ var r0 []domain.IssueEvent
60+ var r1 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.IssueEvent, error)); ok {
62+ return returnFunc(ctx, repo, mr)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.IssueEvent); ok {
65+ r0 = returnFunc(ctx, repo, mr)
66+ } else {
67+ if ret.Get(0) != nil {
68+ r0 = ret.Get(0).([]domain.IssueEvent)
69+ }
70+ }
71+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
72+ r1 = returnFunc(ctx, repo, mr)
73+ } else {
74+ r1 = ret.Error(1)
75+ }
76+ return r0, r1
77+}
78+
79+// IssueLinkStore_EventsOfMergeRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EventsOfMergeRequest'
80+type IssueLinkStore_EventsOfMergeRequest_Call struct {
81+ *mock.Call
82+}
83+
84+// EventsOfMergeRequest is a helper method to define mock.On call
85+// - ctx context.Context
86+// - repo domain.RepoName
87+// - mr int64
88+func (_e *IssueLinkStore_Expecter) EventsOfMergeRequest(ctx any, repo any, mr any) *IssueLinkStore_EventsOfMergeRequest_Call {
89+ return &IssueLinkStore_EventsOfMergeRequest_Call{Call: _e.mock.On("EventsOfMergeRequest", ctx, repo, mr)}
90+}
91+
92+func (_c *IssueLinkStore_EventsOfMergeRequest_Call) Run(run func(ctx context.Context, repo domain.RepoName, mr int64)) *IssueLinkStore_EventsOfMergeRequest_Call {
93+ _c.Call.Run(func(args mock.Arguments) {
94+ var arg0 context.Context
95+ if args[0] != nil {
96+ arg0 = args[0].(context.Context)
97+ }
98+ var arg1 domain.RepoName
99+ if args[1] != nil {
100+ arg1 = args[1].(domain.RepoName)
101+ }
102+ var arg2 int64
103+ if args[2] != nil {
104+ arg2 = args[2].(int64)
105+ }
106+ run(
107+ arg0,
108+ arg1,
109+ arg2,
110+ )
111+ })
112+ return _c
113+}
114+
115+func (_c *IssueLinkStore_EventsOfMergeRequest_Call) Return(issueEvents []domain.IssueEvent, err error) *IssueLinkStore_EventsOfMergeRequest_Call {
116+ _c.Call.Return(issueEvents, err)
117+ return _c
118+}
119+
120+func (_c *IssueLinkStore_EventsOfMergeRequest_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error)) *IssueLinkStore_EventsOfMergeRequest_Call {
121+ _c.Call.Return(run)
122+ return _c
123+}
124+
50 // LinksOfIssue provides a mock function for the type IssueLinkStore125 // LinksOfIssue provides a mock function for the type IssueLinkStore
51 func (_mock *IssueLinkStore) LinksOfIssue(ctx context.Context, repo domain.RepoName, issue int64, limit int) ([]domain.IssueLink, error) {126 func (_mock *IssueLinkStore) LinksOfIssue(ctx context.Context, repo domain.RepoName, issue int64, limit int) ([]domain.IssueLink, error) {
52 ret := _mock.Called(ctx, repo, issue, limit)127 ret := _mock.Called(ctx, repo, issue, limit)
@@ -202,8 +277,8 @@ func (_c *IssueLinkStore_LinksOfMergeRequest_Call) RunAndReturn(run func(ctx con
202 }277 }
203 278
204 // ReplaceLinks provides a mock function for the type IssueLinkStore279 // ReplaceLinks provides a mock function for the type IssueLinkStore
205-func (_mock *IssueLinkStore) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64) ([]domain.IssueLink, []domain.IssueLink, error) {280+func (_mock *IssueLinkStore) ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64, ref ports.ReferenceEvent) ([]domain.IssueLink, []domain.IssueLink, error) {
206- ret := _mock.Called(ctx, repo, mr, issues, closes)281+ ret := _mock.Called(ctx, repo, mr, issues, closes, ref)
207 282
208 if len(ret) == 0 {283 if len(ret) == 0 {
209 panic("no return value specified for ReplaceLinks")284 panic("no return value specified for ReplaceLinks")
@@ -212,25 +287,25 @@ func (_mock *IssueLinkStore) ReplaceLinks(ctx context.Context, repo domain.RepoN
212 var r0 []domain.IssueLink287 var r0 []domain.IssueLink
213 var r1 []domain.IssueLink288 var r1 []domain.IssueLink
214 var r2 error289 var r2 error
215- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64) ([]domain.IssueLink, []domain.IssueLink, error)); ok {290+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) ([]domain.IssueLink, []domain.IssueLink, error)); ok {
216- return returnFunc(ctx, repo, mr, issues, closes)291+ return returnFunc(ctx, repo, mr, issues, closes, ref)
217 }292 }
218- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64) []domain.IssueLink); ok {293+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) []domain.IssueLink); ok {
219- r0 = returnFunc(ctx, repo, mr, issues, closes)294+ r0 = returnFunc(ctx, repo, mr, issues, closes, ref)
220 } else {295 } else {
221 if ret.Get(0) != nil {296 if ret.Get(0) != nil {
222 r0 = ret.Get(0).([]domain.IssueLink)297 r0 = ret.Get(0).([]domain.IssueLink)
223 }298 }
224 }299 }
225- if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, []int64, []int64) []domain.IssueLink); ok {300+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) []domain.IssueLink); ok {
226- r1 = returnFunc(ctx, repo, mr, issues, closes)301+ r1 = returnFunc(ctx, repo, mr, issues, closes, ref)
227 } else {302 } else {
228 if ret.Get(1) != nil {303 if ret.Get(1) != nil {
229 r1 = ret.Get(1).([]domain.IssueLink)304 r1 = ret.Get(1).([]domain.IssueLink)
230 }305 }
231 }306 }
232- if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, int64, []int64, []int64) error); ok {307+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, int64, []int64, []int64, ports.ReferenceEvent) error); ok {
233- r2 = returnFunc(ctx, repo, mr, issues, closes)308+ r2 = returnFunc(ctx, repo, mr, issues, closes, ref)
234 } else {309 } else {
235 r2 = ret.Error(2)310 r2 = ret.Error(2)
236 }311 }
@@ -248,11 +323,12 @@ type IssueLinkStore_ReplaceLinks_Call struct {
248 // - mr int64323 // - mr int64
249 // - issues []int64324 // - issues []int64
250 // - closes []int64325 // - closes []int64
251-func (_e *IssueLinkStore_Expecter) ReplaceLinks(ctx any, repo any, mr any, issues any, closes any) *IssueLinkStore_ReplaceLinks_Call {326+// - ref ports.ReferenceEvent
252- return &IssueLinkStore_ReplaceLinks_Call{Call: _e.mock.On("ReplaceLinks", ctx, repo, mr, issues, closes)}327+func (_e *IssueLinkStore_Expecter) ReplaceLinks(ctx any, repo any, mr any, issues any, closes any, ref any) *IssueLinkStore_ReplaceLinks_Call {
328+ return &IssueLinkStore_ReplaceLinks_Call{Call: _e.mock.On("ReplaceLinks", ctx, repo, mr, issues, closes, ref)}
253 }329 }
254 330
255-func (_c *IssueLinkStore_ReplaceLinks_Call) Run(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64)) *IssueLinkStore_ReplaceLinks_Call {331+func (_c *IssueLinkStore_ReplaceLinks_Call) Run(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64, ref ports.ReferenceEvent)) *IssueLinkStore_ReplaceLinks_Call {
256 _c.Call.Run(func(args mock.Arguments) {332 _c.Call.Run(func(args mock.Arguments) {
257 var arg0 context.Context333 var arg0 context.Context
258 if args[0] != nil {334 if args[0] != nil {
@@ -274,12 +350,17 @@ func (_c *IssueLinkStore_ReplaceLinks_Call) Run(run func(ctx context.Context, re
274 if args[4] != nil {350 if args[4] != nil {
275 arg4 = args[4].([]int64)351 arg4 = args[4].([]int64)
276 }352 }
353+ var arg5 ports.ReferenceEvent
354+ if args[5] != nil {
355+ arg5 = args[5].(ports.ReferenceEvent)
356+ }
277 run(357 run(
278 arg0,358 arg0,
279 arg1,359 arg1,
280 arg2,360 arg2,
281 arg3,361 arg3,
282 arg4,362 arg4,
363+ arg5,
283 )364 )
284 })365 })
285 return _c366 return _c
@@ -290,7 +371,7 @@ func (_c *IssueLinkStore_ReplaceLinks_Call) Return(added []domain.IssueLink, rem
290 return _c371 return _c
291 }372 }
292 373
293-func (_c *IssueLinkStore_ReplaceLinks_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64) ([]domain.IssueLink, []domain.IssueLink, error)) *IssueLinkStore_ReplaceLinks_Call {374+func (_c *IssueLinkStore_ReplaceLinks_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, mr int64, issues []int64, closes []int64, ref ports.ReferenceEvent) ([]domain.IssueLink, []domain.IssueLink, error)) *IssueLinkStore_ReplaceLinks_Call {
294 _c.Call.Return(run)375 _c.Call.Return(run)
295 return _c376 return _c
296 }377 }
M internal/ports/mocks/IssueLinkTrigger.go +32 -20
@@ -48,8 +48,8 @@ func (_m *IssueLinkTrigger) EXPECT() *IssueLinkTrigger_Expecter {
4848 }
4949
5050 // OnBranchesMoved provides a mock function for the type IssueLinkTrigger
51-func (_mock *IssueLinkTrigger) OnBranchesMoved(context1 context.Context, repoName domain.RepoName, refUpdates []domain.RefUpdate) {
52- _mock.Called(context1, repoName, refUpdates)
51+func (_mock *IssueLinkTrigger) OnBranchesMoved(context1 context.Context, principal domain.Principal, repoName domain.RepoName, refUpdates []domain.RefUpdate) {
52+ _mock.Called(context1, principal, repoName, refUpdates)
5353 return
5454 }
5555
@@ -60,30 +60,36 @@ type IssueLinkTrigger_OnBranchesMoved_Call struct {
6060
6161 // OnBranchesMoved is a helper method to define mock.On call
6262 // - context1 context.Context
63+// - principal domain.Principal
6364 // - repoName domain.RepoName
6465 // - refUpdates []domain.RefUpdate
65-func (_e *IssueLinkTrigger_Expecter) OnBranchesMoved(context1 any, repoName any, refUpdates any) *IssueLinkTrigger_OnBranchesMoved_Call {
66- return &IssueLinkTrigger_OnBranchesMoved_Call{Call: _e.mock.On("OnBranchesMoved", context1, repoName, refUpdates)}
66+func (_e *IssueLinkTrigger_Expecter) OnBranchesMoved(context1 any, principal any, repoName any, refUpdates any) *IssueLinkTrigger_OnBranchesMoved_Call {
67+ return &IssueLinkTrigger_OnBranchesMoved_Call{Call: _e.mock.On("OnBranchesMoved", context1, principal, repoName, refUpdates)}
6768 }
6869
69-func (_c *IssueLinkTrigger_OnBranchesMoved_Call) Run(run func(context1 context.Context, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {
70+func (_c *IssueLinkTrigger_OnBranchesMoved_Call) Run(run func(context1 context.Context, principal domain.Principal, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {
7071 _c.Call.Run(func(args mock.Arguments) {
7172 var arg0 context.Context
7273 if args[0] != nil {
7374 arg0 = args[0].(context.Context)
7475 }
75- var arg1 domain.RepoName
76+ var arg1 domain.Principal
7677 if args[1] != nil {
77- arg1 = args[1].(domain.RepoName)
78+ arg1 = args[1].(domain.Principal)
7879 }
79- var arg2 []domain.RefUpdate
80+ var arg2 domain.RepoName
8081 if args[2] != nil {
81- arg2 = args[2].([]domain.RefUpdate)
82+ arg2 = args[2].(domain.RepoName)
83+ }
84+ var arg3 []domain.RefUpdate
85+ if args[3] != nil {
86+ arg3 = args[3].([]domain.RefUpdate)
8287 }
8388 run(
8489 arg0,
8590 arg1,
8691 arg2,
92+ arg3,
8793 )
8894 })
8995 return _c
@@ -94,14 +100,14 @@ func (_c *IssueLinkTrigger_OnBranchesMoved_Call) Return() *IssueLinkTrigger_OnBr
94100 return _c
95101 }
96102
97-func (_c *IssueLinkTrigger_OnBranchesMoved_Call) RunAndReturn(run func(context1 context.Context, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {
103+func (_c *IssueLinkTrigger_OnBranchesMoved_Call) RunAndReturn(run func(context1 context.Context, principal domain.Principal, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {
98104 _c.Run(run)
99105 return _c
100106 }
101107
102108 // OnMergeRequestChanged provides a mock function for the type IssueLinkTrigger
103-func (_mock *IssueLinkTrigger) OnMergeRequestChanged(context1 context.Context, repo domain.Repo, n int64) {
104- _mock.Called(context1, repo, n)
109+func (_mock *IssueLinkTrigger) OnMergeRequestChanged(context1 context.Context, principal domain.Principal, repo domain.Repo, n int64) {
110+ _mock.Called(context1, principal, repo, n)
105111 return
106112 }
107113
@@ -112,30 +118,36 @@ type IssueLinkTrigger_OnMergeRequestChanged_Call struct {
112118
113119 // OnMergeRequestChanged is a helper method to define mock.On call
114120 // - context1 context.Context
121+// - principal domain.Principal
115122 // - repo domain.Repo
116123 // - n int64
117-func (_e *IssueLinkTrigger_Expecter) OnMergeRequestChanged(context1 any, repo any, n any) *IssueLinkTrigger_OnMergeRequestChanged_Call {
118- return &IssueLinkTrigger_OnMergeRequestChanged_Call{Call: _e.mock.On("OnMergeRequestChanged", context1, repo, n)}
124+func (_e *IssueLinkTrigger_Expecter) OnMergeRequestChanged(context1 any, principal any, repo any, n any) *IssueLinkTrigger_OnMergeRequestChanged_Call {
125+ return &IssueLinkTrigger_OnMergeRequestChanged_Call{Call: _e.mock.On("OnMergeRequestChanged", context1, principal, repo, n)}
119126 }
120127
121-func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) Run(run func(context1 context.Context, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {
128+func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) Run(run func(context1 context.Context, principal domain.Principal, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {
122129 _c.Call.Run(func(args mock.Arguments) {
123130 var arg0 context.Context
124131 if args[0] != nil {
125132 arg0 = args[0].(context.Context)
126133 }
127- var arg1 domain.Repo
134+ var arg1 domain.Principal
128135 if args[1] != nil {
129- arg1 = args[1].(domain.Repo)
136+ arg1 = args[1].(domain.Principal)
130137 }
131- var arg2 int64
138+ var arg2 domain.Repo
132139 if args[2] != nil {
133- arg2 = args[2].(int64)
140+ arg2 = args[2].(domain.Repo)
141+ }
142+ var arg3 int64
143+ if args[3] != nil {
144+ arg3 = args[3].(int64)
134145 }
135146 run(
136147 arg0,
137148 arg1,
138149 arg2,
150+ arg3,
139151 )
140152 })
141153 return _c
@@ -146,7 +158,7 @@ func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) Return() *IssueLinkTrigge
146158 return _c
147159 }
148160
149-func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) RunAndReturn(run func(context1 context.Context, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {
161+func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) RunAndReturn(run func(context1 context.Context, principal domain.Principal, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {
150162 _c.Run(run)
151163 return _c
152164 }
@@ -48,8 +48,8 @@ func (_m *IssueLinkTrigger) EXPECT() *IssueLinkTrigger_Expecter {
48 }48 }
49 49
50 // OnBranchesMoved provides a mock function for the type IssueLinkTrigger50 // OnBranchesMoved provides a mock function for the type IssueLinkTrigger
51-func (_mock *IssueLinkTrigger) OnBranchesMoved(context1 context.Context, repoName domain.RepoName, refUpdates []domain.RefUpdate) {51+func (_mock *IssueLinkTrigger) OnBranchesMoved(context1 context.Context, principal domain.Principal, repoName domain.RepoName, refUpdates []domain.RefUpdate) {
52- _mock.Called(context1, repoName, refUpdates)52+ _mock.Called(context1, principal, repoName, refUpdates)
53 return53 return
54 }54 }
55 55
@@ -60,30 +60,36 @@ type IssueLinkTrigger_OnBranchesMoved_Call struct {
60 60
61 // OnBranchesMoved is a helper method to define mock.On call61 // OnBranchesMoved is a helper method to define mock.On call
62 // - context1 context.Context62 // - context1 context.Context
63+// - principal domain.Principal
63 // - repoName domain.RepoName64 // - repoName domain.RepoName
64 // - refUpdates []domain.RefUpdate65 // - refUpdates []domain.RefUpdate
65-func (_e *IssueLinkTrigger_Expecter) OnBranchesMoved(context1 any, repoName any, refUpdates any) *IssueLinkTrigger_OnBranchesMoved_Call {66+func (_e *IssueLinkTrigger_Expecter) OnBranchesMoved(context1 any, principal any, repoName any, refUpdates any) *IssueLinkTrigger_OnBranchesMoved_Call {
66- return &IssueLinkTrigger_OnBranchesMoved_Call{Call: _e.mock.On("OnBranchesMoved", context1, repoName, refUpdates)}67+ return &IssueLinkTrigger_OnBranchesMoved_Call{Call: _e.mock.On("OnBranchesMoved", context1, principal, repoName, refUpdates)}
67 }68 }
68 69
69-func (_c *IssueLinkTrigger_OnBranchesMoved_Call) Run(run func(context1 context.Context, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {70+func (_c *IssueLinkTrigger_OnBranchesMoved_Call) Run(run func(context1 context.Context, principal domain.Principal, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {
70 _c.Call.Run(func(args mock.Arguments) {71 _c.Call.Run(func(args mock.Arguments) {
71 var arg0 context.Context72 var arg0 context.Context
72 if args[0] != nil {73 if args[0] != nil {
73 arg0 = args[0].(context.Context)74 arg0 = args[0].(context.Context)
74 }75 }
75- var arg1 domain.RepoName76+ var arg1 domain.Principal
76 if args[1] != nil {77 if args[1] != nil {
77- arg1 = args[1].(domain.RepoName)78+ arg1 = args[1].(domain.Principal)
78 }79 }
79- var arg2 []domain.RefUpdate80+ var arg2 domain.RepoName
80 if args[2] != nil {81 if args[2] != nil {
81- arg2 = args[2].([]domain.RefUpdate)82+ arg2 = args[2].(domain.RepoName)
83+ }
84+ var arg3 []domain.RefUpdate
85+ if args[3] != nil {
86+ arg3 = args[3].([]domain.RefUpdate)
82 }87 }
83 run(88 run(
84 arg0,89 arg0,
85 arg1,90 arg1,
86 arg2,91 arg2,
92+ arg3,
87 )93 )
88 })94 })
89 return _c95 return _c
@@ -94,14 +100,14 @@ func (_c *IssueLinkTrigger_OnBranchesMoved_Call) Return() *IssueLinkTrigger_OnBr
94 return _c100 return _c
95 }101 }
96 102
97-func (_c *IssueLinkTrigger_OnBranchesMoved_Call) RunAndReturn(run func(context1 context.Context, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {103+func (_c *IssueLinkTrigger_OnBranchesMoved_Call) RunAndReturn(run func(context1 context.Context, principal domain.Principal, repoName domain.RepoName, refUpdates []domain.RefUpdate)) *IssueLinkTrigger_OnBranchesMoved_Call {
98 _c.Run(run)104 _c.Run(run)
99 return _c105 return _c
100 }106 }
101 107
102 // OnMergeRequestChanged provides a mock function for the type IssueLinkTrigger108 // OnMergeRequestChanged provides a mock function for the type IssueLinkTrigger
103-func (_mock *IssueLinkTrigger) OnMergeRequestChanged(context1 context.Context, repo domain.Repo, n int64) {109+func (_mock *IssueLinkTrigger) OnMergeRequestChanged(context1 context.Context, principal domain.Principal, repo domain.Repo, n int64) {
104- _mock.Called(context1, repo, n)110+ _mock.Called(context1, principal, repo, n)
105 return111 return
106 }112 }
107 113
@@ -112,30 +118,36 @@ type IssueLinkTrigger_OnMergeRequestChanged_Call struct {
112 118
113 // OnMergeRequestChanged is a helper method to define mock.On call119 // OnMergeRequestChanged is a helper method to define mock.On call
114 // - context1 context.Context120 // - context1 context.Context
121+// - principal domain.Principal
115 // - repo domain.Repo122 // - repo domain.Repo
116 // - n int64123 // - n int64
117-func (_e *IssueLinkTrigger_Expecter) OnMergeRequestChanged(context1 any, repo any, n any) *IssueLinkTrigger_OnMergeRequestChanged_Call {124+func (_e *IssueLinkTrigger_Expecter) OnMergeRequestChanged(context1 any, principal any, repo any, n any) *IssueLinkTrigger_OnMergeRequestChanged_Call {
118- return &IssueLinkTrigger_OnMergeRequestChanged_Call{Call: _e.mock.On("OnMergeRequestChanged", context1, repo, n)}125+ return &IssueLinkTrigger_OnMergeRequestChanged_Call{Call: _e.mock.On("OnMergeRequestChanged", context1, principal, repo, n)}
119 }126 }
120 127
121-func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) Run(run func(context1 context.Context, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {128+func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) Run(run func(context1 context.Context, principal domain.Principal, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {
122 _c.Call.Run(func(args mock.Arguments) {129 _c.Call.Run(func(args mock.Arguments) {
123 var arg0 context.Context130 var arg0 context.Context
124 if args[0] != nil {131 if args[0] != nil {
125 arg0 = args[0].(context.Context)132 arg0 = args[0].(context.Context)
126 }133 }
127- var arg1 domain.Repo134+ var arg1 domain.Principal
128 if args[1] != nil {135 if args[1] != nil {
129- arg1 = args[1].(domain.Repo)136+ arg1 = args[1].(domain.Principal)
130 }137 }
131- var arg2 int64138+ var arg2 domain.Repo
132 if args[2] != nil {139 if args[2] != nil {
133- arg2 = args[2].(int64)140+ arg2 = args[2].(domain.Repo)
141+ }
142+ var arg3 int64
143+ if args[3] != nil {
144+ arg3 = args[3].(int64)
134 }145 }
135 run(146 run(
136 arg0,147 arg0,
137 arg1,148 arg1,
138 arg2,149 arg2,
150+ arg3,
139 )151 )
140 })152 })
141 return _c153 return _c
@@ -146,7 +158,7 @@ func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) Return() *IssueLinkTrigge
146 return _c158 return _c
147 }159 }
148 160
149-func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) RunAndReturn(run func(context1 context.Context, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {161+func (_c *IssueLinkTrigger_OnMergeRequestChanged_Call) RunAndReturn(run func(context1 context.Context, principal domain.Principal, repo domain.Repo, n int64)) *IssueLinkTrigger_OnMergeRequestChanged_Call {
150 _c.Run(run)162 _c.Run(run)
151 return _c163 return _c
152 }164 }
M internal/ports/mocks/IssueService.go +78 -0
@@ -521,3 +521,81 @@ func (_c *IssueService_Reopen_Call) RunAndReturn(run func(ctx context.Context, p
521521 _c.Call.Return(run)
522522 return _c
523523 }
524+
525+// Resolve provides a mock function for the type IssueService
526+func (_mock *IssueService) Resolve(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error) {
527+ ret := _mock.Called(ctx, p, name, number)
528+
529+ if len(ret) == 0 {
530+ panic("no return value specified for Resolve")
531+ }
532+
533+ var r0 domain.Issue
534+ var r1 error
535+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) (domain.Issue, error)); ok {
536+ return returnFunc(ctx, p, name, number)
537+ }
538+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) domain.Issue); ok {
539+ r0 = returnFunc(ctx, p, name, number)
540+ } else {
541+ r0 = ret.Get(0).(domain.Issue)
542+ }
543+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
544+ r1 = returnFunc(ctx, p, name, number)
545+ } else {
546+ r1 = ret.Error(1)
547+ }
548+ return r0, r1
549+}
550+
551+// IssueService_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve'
552+type IssueService_Resolve_Call struct {
553+ *mock.Call
554+}
555+
556+// Resolve is a helper method to define mock.On call
557+// - ctx context.Context
558+// - p domain.Principal
559+// - name domain.RepoName
560+// - number int64
561+func (_e *IssueService_Expecter) Resolve(ctx any, p any, name any, number any) *IssueService_Resolve_Call {
562+ return &IssueService_Resolve_Call{Call: _e.mock.On("Resolve", ctx, p, name, number)}
563+}
564+
565+func (_c *IssueService_Resolve_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64)) *IssueService_Resolve_Call {
566+ _c.Call.Run(func(args mock.Arguments) {
567+ var arg0 context.Context
568+ if args[0] != nil {
569+ arg0 = args[0].(context.Context)
570+ }
571+ var arg1 domain.Principal
572+ if args[1] != nil {
573+ arg1 = args[1].(domain.Principal)
574+ }
575+ var arg2 domain.RepoName
576+ if args[2] != nil {
577+ arg2 = args[2].(domain.RepoName)
578+ }
579+ var arg3 int64
580+ if args[3] != nil {
581+ arg3 = args[3].(int64)
582+ }
583+ run(
584+ arg0,
585+ arg1,
586+ arg2,
587+ arg3,
588+ )
589+ })
590+ return _c
591+}
592+
593+func (_c *IssueService_Resolve_Call) Return(issue domain.Issue, err error) *IssueService_Resolve_Call {
594+ _c.Call.Return(issue, err)
595+ return _c
596+}
597+
598+func (_c *IssueService_Resolve_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)) *IssueService_Resolve_Call {
599+ _c.Call.Return(run)
600+ return _c
601+}
@@ -521,3 +521,81 @@ func (_c *IssueService_Reopen_Call) RunAndReturn(run func(ctx context.Context, p
521 _c.Call.Return(run)521 _c.Call.Return(run)
522 return _c522 return _c
523 }523 }
524+
525+// Resolve provides a mock function for the type IssueService
526+func (_mock *IssueService) Resolve(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error) {
527+ ret := _mock.Called(ctx, p, name, number)
528+
529+ if len(ret) == 0 {
530+ panic("no return value specified for Resolve")
531+ }
532+
533+ var r0 domain.Issue
534+ var r1 error
535+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) (domain.Issue, error)); ok {
536+ return returnFunc(ctx, p, name, number)
537+ }
538+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) domain.Issue); ok {
539+ r0 = returnFunc(ctx, p, name, number)
540+ } else {
541+ r0 = ret.Get(0).(domain.Issue)
542+ }
543+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
544+ r1 = returnFunc(ctx, p, name, number)
545+ } else {
546+ r1 = ret.Error(1)
547+ }
548+ return r0, r1
549+}
550+
551+// IssueService_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve'
552+type IssueService_Resolve_Call struct {
553+ *mock.Call
554+}
555+
556+// Resolve is a helper method to define mock.On call
557+// - ctx context.Context
558+// - p domain.Principal
559+// - name domain.RepoName
560+// - number int64
561+func (_e *IssueService_Expecter) Resolve(ctx any, p any, name any, number any) *IssueService_Resolve_Call {
562+ return &IssueService_Resolve_Call{Call: _e.mock.On("Resolve", ctx, p, name, number)}
563+}
564+
565+func (_c *IssueService_Resolve_Call) Run(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64)) *IssueService_Resolve_Call {
566+ _c.Call.Run(func(args mock.Arguments) {
567+ var arg0 context.Context
568+ if args[0] != nil {
569+ arg0 = args[0].(context.Context)
570+ }
571+ var arg1 domain.Principal
572+ if args[1] != nil {
573+ arg1 = args[1].(domain.Principal)
574+ }
575+ var arg2 domain.RepoName
576+ if args[2] != nil {
577+ arg2 = args[2].(domain.RepoName)
578+ }
579+ var arg3 int64
580+ if args[3] != nil {
581+ arg3 = args[3].(int64)
582+ }
583+ run(
584+ arg0,
585+ arg1,
586+ arg2,
587+ arg3,
588+ )
589+ })
590+ return _c
591+}
592+
593+func (_c *IssueService_Resolve_Call) Return(issue domain.Issue, err error) *IssueService_Resolve_Call {
594+ _c.Call.Return(issue, err)
595+ return _c
596+}
597+
598+func (_c *IssueService_Resolve_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)) *IssueService_Resolve_Call {
599+ _c.Call.Return(run)
600+ return _c
601+}
M internal/ports/mocks/IssueStore.go +97 -8
@@ -332,6 +332,80 @@ func (_c *IssueStore_ListComments_Call) RunAndReturn(run func(ctx context.Contex
332332 return _c
333333 }
334334
335+// ListEvents provides a mock function for the type IssueStore
336+func (_mock *IssueStore) ListEvents(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error) {
337+ ret := _mock.Called(ctx, repo, number)
338+
339+ if len(ret) == 0 {
340+ panic("no return value specified for ListEvents")
341+ }
342+
343+ var r0 []domain.IssueEvent
344+ var r1 error
345+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.IssueEvent, error)); ok {
346+ return returnFunc(ctx, repo, number)
347+ }
348+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.IssueEvent); ok {
349+ r0 = returnFunc(ctx, repo, number)
350+ } else {
351+ if ret.Get(0) != nil {
352+ r0 = ret.Get(0).([]domain.IssueEvent)
353+ }
354+ }
355+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
356+ r1 = returnFunc(ctx, repo, number)
357+ } else {
358+ r1 = ret.Error(1)
359+ }
360+ return r0, r1
361+}
362+
363+// IssueStore_ListEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListEvents'
364+type IssueStore_ListEvents_Call struct {
365+ *mock.Call
366+}
367+
368+// ListEvents is a helper method to define mock.On call
369+// - ctx context.Context
370+// - repo domain.RepoName
371+// - number int64
372+func (_e *IssueStore_Expecter) ListEvents(ctx any, repo any, number any) *IssueStore_ListEvents_Call {
373+ return &IssueStore_ListEvents_Call{Call: _e.mock.On("ListEvents", ctx, repo, number)}
374+}
375+
376+func (_c *IssueStore_ListEvents_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *IssueStore_ListEvents_Call {
377+ _c.Call.Run(func(args mock.Arguments) {
378+ var arg0 context.Context
379+ if args[0] != nil {
380+ arg0 = args[0].(context.Context)
381+ }
382+ var arg1 domain.RepoName
383+ if args[1] != nil {
384+ arg1 = args[1].(domain.RepoName)
385+ }
386+ var arg2 int64
387+ if args[2] != nil {
388+ arg2 = args[2].(int64)
389+ }
390+ run(
391+ arg0,
392+ arg1,
393+ arg2,
394+ )
395+ })
396+ return _c
397+}
398+
399+func (_c *IssueStore_ListEvents_Call) Return(issueEvents []domain.IssueEvent, err error) *IssueStore_ListEvents_Call {
400+ _c.Call.Return(issueEvents, err)
401+ return _c
402+}
403+
404+func (_c *IssueStore_ListEvents_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error)) *IssueStore_ListEvents_Call {
405+ _c.Call.Return(run)
406+ return _c
407+}
408+
335409 // ListOpenAll provides a mock function for the type IssueStore
336410 func (_mock *IssueStore) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
337411 ret := _mock.Called(ctx)
@@ -458,16 +532,22 @@ func (_c *IssueStore_SaveComment_Call) RunAndReturn(run func(ctx context.Context
458532 }
459533
460534 // Update provides a mock function for the type IssueStore
461-func (_mock *IssueStore) Update(ctx context.Context, issue domain.Issue) error {
462- ret := _mock.Called(ctx, issue)
535+func (_mock *IssueStore) Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error {
536+ var tmpRet mock.Arguments
537+ if len(events) > 0 {
538+ tmpRet = _mock.Called(ctx, issue, events)
539+ } else {
540+ tmpRet = _mock.Called(ctx, issue)
541+ }
542+ ret := tmpRet
463543
464544 if len(ret) == 0 {
465545 panic("no return value specified for Update")
466546 }
467547
468548 var r0 error
469- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Issue) error); ok {
470- r0 = returnFunc(ctx, issue)
549+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Issue, ...domain.IssueEvent) error); ok {
550+ r0 = returnFunc(ctx, issue, events...)
471551 } else {
472552 r0 = ret.Error(0)
473553 }
@@ -482,11 +562,13 @@ type IssueStore_Update_Call struct {
482562 // Update is a helper method to define mock.On call
483563 // - ctx context.Context
484564 // - issue domain.Issue
485-func (_e *IssueStore_Expecter) Update(ctx any, issue any) *IssueStore_Update_Call {
486- return &IssueStore_Update_Call{Call: _e.mock.On("Update", ctx, issue)}
565+// - events ...domain.IssueEvent
566+func (_e *IssueStore_Expecter) Update(ctx any, issue any, events ...any) *IssueStore_Update_Call {
567+ return &IssueStore_Update_Call{Call: _e.mock.On("Update",
568+ append([]any{ctx, issue}, events...)...)}
487569 }
488570
489-func (_c *IssueStore_Update_Call) Run(run func(ctx context.Context, issue domain.Issue)) *IssueStore_Update_Call {
571+func (_c *IssueStore_Update_Call) Run(run func(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent)) *IssueStore_Update_Call {
490572 _c.Call.Run(func(args mock.Arguments) {
491573 var arg0 context.Context
492574 if args[0] != nil {
@@ -496,9 +578,16 @@ func (_c *IssueStore_Update_Call) Run(run func(ctx context.Context, issue domain
496578 if args[1] != nil {
497579 arg1 = args[1].(domain.Issue)
498580 }
581+ var arg2 []domain.IssueEvent
582+ var variadicArgs []domain.IssueEvent
583+ if len(args) > 2 {
584+ variadicArgs = args[2].([]domain.IssueEvent)
585+ }
586+ arg2 = variadicArgs
499587 run(
500588 arg0,
501589 arg1,
590+ arg2...,
502591 )
503592 })
504593 return _c
@@ -509,7 +598,7 @@ func (_c *IssueStore_Update_Call) Return(err error) *IssueStore_Update_Call {
509598 return _c
510599 }
511600
512-func (_c *IssueStore_Update_Call) RunAndReturn(run func(ctx context.Context, issue domain.Issue) error) *IssueStore_Update_Call {
601+func (_c *IssueStore_Update_Call) RunAndReturn(run func(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error) *IssueStore_Update_Call {
513602 _c.Call.Return(run)
514603 return _c
515604 }
@@ -332,6 +332,80 @@ func (_c *IssueStore_ListComments_Call) RunAndReturn(run func(ctx context.Contex
332 return _c332 return _c
333 }333 }
334 334
335+// ListEvents provides a mock function for the type IssueStore
336+func (_mock *IssueStore) ListEvents(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error) {
337+ ret := _mock.Called(ctx, repo, number)
338+
339+ if len(ret) == 0 {
340+ panic("no return value specified for ListEvents")
341+ }
342+
343+ var r0 []domain.IssueEvent
344+ var r1 error
345+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) ([]domain.IssueEvent, error)); ok {
346+ return returnFunc(ctx, repo, number)
347+ }
348+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) []domain.IssueEvent); ok {
349+ r0 = returnFunc(ctx, repo, number)
350+ } else {
351+ if ret.Get(0) != nil {
352+ r0 = ret.Get(0).([]domain.IssueEvent)
353+ }
354+ }
355+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
356+ r1 = returnFunc(ctx, repo, number)
357+ } else {
358+ r1 = ret.Error(1)
359+ }
360+ return r0, r1
361+}
362+
363+// IssueStore_ListEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListEvents'
364+type IssueStore_ListEvents_Call struct {
365+ *mock.Call
366+}
367+
368+// ListEvents is a helper method to define mock.On call
369+// - ctx context.Context
370+// - repo domain.RepoName
371+// - number int64
372+func (_e *IssueStore_Expecter) ListEvents(ctx any, repo any, number any) *IssueStore_ListEvents_Call {
373+ return &IssueStore_ListEvents_Call{Call: _e.mock.On("ListEvents", ctx, repo, number)}
374+}
375+
376+func (_c *IssueStore_ListEvents_Call) Run(run func(ctx context.Context, repo domain.RepoName, number int64)) *IssueStore_ListEvents_Call {
377+ _c.Call.Run(func(args mock.Arguments) {
378+ var arg0 context.Context
379+ if args[0] != nil {
380+ arg0 = args[0].(context.Context)
381+ }
382+ var arg1 domain.RepoName
383+ if args[1] != nil {
384+ arg1 = args[1].(domain.RepoName)
385+ }
386+ var arg2 int64
387+ if args[2] != nil {
388+ arg2 = args[2].(int64)
389+ }
390+ run(
391+ arg0,
392+ arg1,
393+ arg2,
394+ )
395+ })
396+ return _c
397+}
398+
399+func (_c *IssueStore_ListEvents_Call) Return(issueEvents []domain.IssueEvent, err error) *IssueStore_ListEvents_Call {
400+ _c.Call.Return(issueEvents, err)
401+ return _c
402+}
403+
404+func (_c *IssueStore_ListEvents_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error)) *IssueStore_ListEvents_Call {
405+ _c.Call.Return(run)
406+ return _c
407+}
408+
335 // ListOpenAll provides a mock function for the type IssueStore409 // ListOpenAll provides a mock function for the type IssueStore
336 func (_mock *IssueStore) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {410 func (_mock *IssueStore) ListOpenAll(ctx context.Context) ([]domain.Issue, error) {
337 ret := _mock.Called(ctx)411 ret := _mock.Called(ctx)
@@ -458,16 +532,22 @@ func (_c *IssueStore_SaveComment_Call) RunAndReturn(run func(ctx context.Context
458 }532 }
459 533
460 // Update provides a mock function for the type IssueStore534 // Update provides a mock function for the type IssueStore
461-func (_mock *IssueStore) Update(ctx context.Context, issue domain.Issue) error {535+func (_mock *IssueStore) Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error {
462- ret := _mock.Called(ctx, issue)536+ var tmpRet mock.Arguments
537+ if len(events) > 0 {
538+ tmpRet = _mock.Called(ctx, issue, events)
539+ } else {
540+ tmpRet = _mock.Called(ctx, issue)
541+ }
542+ ret := tmpRet
463 543
464 if len(ret) == 0 {544 if len(ret) == 0 {
465 panic("no return value specified for Update")545 panic("no return value specified for Update")
466 }546 }
467 547
468 var r0 error548 var r0 error
469- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Issue) error); ok {549+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Issue, ...domain.IssueEvent) error); ok {
470- r0 = returnFunc(ctx, issue)550+ r0 = returnFunc(ctx, issue, events...)
471 } else {551 } else {
472 r0 = ret.Error(0)552 r0 = ret.Error(0)
473 }553 }
@@ -482,11 +562,13 @@ type IssueStore_Update_Call struct {
482 // Update is a helper method to define mock.On call562 // Update is a helper method to define mock.On call
483 // - ctx context.Context563 // - ctx context.Context
484 // - issue domain.Issue564 // - issue domain.Issue
485-func (_e *IssueStore_Expecter) Update(ctx any, issue any) *IssueStore_Update_Call {565+// - events ...domain.IssueEvent
486- return &IssueStore_Update_Call{Call: _e.mock.On("Update", ctx, issue)}566+func (_e *IssueStore_Expecter) Update(ctx any, issue any, events ...any) *IssueStore_Update_Call {
567+ return &IssueStore_Update_Call{Call: _e.mock.On("Update",
568+ append([]any{ctx, issue}, events...)...)}
487 }569 }
488 570
489-func (_c *IssueStore_Update_Call) Run(run func(ctx context.Context, issue domain.Issue)) *IssueStore_Update_Call {571+func (_c *IssueStore_Update_Call) Run(run func(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent)) *IssueStore_Update_Call {
490 _c.Call.Run(func(args mock.Arguments) {572 _c.Call.Run(func(args mock.Arguments) {
491 var arg0 context.Context573 var arg0 context.Context
492 if args[0] != nil {574 if args[0] != nil {
@@ -496,9 +578,16 @@ func (_c *IssueStore_Update_Call) Run(run func(ctx context.Context, issue domain
496 if args[1] != nil {578 if args[1] != nil {
497 arg1 = args[1].(domain.Issue)579 arg1 = args[1].(domain.Issue)
498 }580 }
581+ var arg2 []domain.IssueEvent
582+ var variadicArgs []domain.IssueEvent
583+ if len(args) > 2 {
584+ variadicArgs = args[2].([]domain.IssueEvent)
585+ }
586+ arg2 = variadicArgs
499 run(587 run(
500 arg0,588 arg0,
501 arg1,589 arg1,
590+ arg2...,
502 )591 )
503 })592 })
504 return _c593 return _c
@@ -509,7 +598,7 @@ func (_c *IssueStore_Update_Call) Return(err error) *IssueStore_Update_Call {
509 return _c598 return _c
510 }599 }
511 600
512-func (_c *IssueStore_Update_Call) RunAndReturn(run func(ctx context.Context, issue domain.Issue) error) *IssueStore_Update_Call {601+func (_c *IssueStore_Update_Call) RunAndReturn(run func(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error) *IssueStore_Update_Call {
513 _c.Call.Return(run)602 _c.Call.Return(run)
514 return _c603 return _c
515 }604 }
M internal/ports/ports.go +12 -4
@@ -267,7 +267,11 @@ type IssueStore interface {
267267 // Update replaces the stored issue and keeps the index of the open issues
268268 // in step. It returns domain.ErrNotFound if there is none and
269269 // domain.ErrConflict if another write changed it since issue was read.
270- Update(ctx context.Context, issue domain.Issue) error
270+ // Events are appended atomically with the write, with sequence IDs assigned
271+ // by the store. Events beyond kvstore.MaxIssueEvents (500) are silently dropped.
272+ Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error
273+ // ListEvents returns history oldest first.
274+ ListEvents(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error)
271275 // SaveComment writes a comment and the issue it belongs to in one batch: a
272276 // new comment (Version 0) must not exist yet, an existing one must be
273277 // unchanged since it was read, and so must issue. It returns
@@ -548,7 +552,11 @@ type IssueLinkStore interface {
548552 // ReplaceLinks atomically replaces an MR's links (closes is a subset of issues).
549553 // Added includes new links and links whose closes flag turned on.
550554 // Returns ErrNotFound for a missing repository, ErrConflict on a race.
551- ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64) (added, removed []domain.IssueLink, err error)
555+ ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64, ref ReferenceEvent) (added, removed []domain.IssueLink, err error)
556+ // EventsOfMergeRequest returns at most 500 events, oldest CreatedAt first,
557+ // breaking ties by issue number then sequence ID. Events can name issues
558+ // absent from the current links.
559+ EventsOfMergeRequest(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error)
552560 // LinksOfIssue returns links ascending by MR number, at most limit.
553561 LinksOfIssue(ctx context.Context, repo domain.RepoName, issue int64, limit int) ([]domain.IssueLink, error)
554562 // LinksOfMergeRequest returns the issues named by an MR.
@@ -562,8 +570,8 @@ type Clock interface {
562570
563571 // IssueLinkTrigger updates derived links without failing the primary operation.
564572 type IssueLinkTrigger interface {
565- OnMergeRequestChanged(context.Context, domain.Repo, int64)
573+ OnMergeRequestChanged(context.Context, domain.Principal, domain.Repo, int64)
566574 OnMergeRequestSettled(context.Context, domain.Repo, domain.MergeRequest)
567- OnBranchesMoved(context.Context, domain.RepoName, []domain.RefUpdate)
575+ OnBranchesMoved(context.Context, domain.Principal, domain.RepoName, []domain.RefUpdate)
568576 OnMerged(context.Context, domain.Principal, domain.Repo, domain.MergeRequest)
569577 }
@@ -267,7 +267,11 @@ type IssueStore interface {
267 // Update replaces the stored issue and keeps the index of the open issues267 // Update replaces the stored issue and keeps the index of the open issues
268 // in step. It returns domain.ErrNotFound if there is none and268 // in step. It returns domain.ErrNotFound if there is none and
269 // domain.ErrConflict if another write changed it since issue was read.269 // domain.ErrConflict if another write changed it since issue was read.
270- Update(ctx context.Context, issue domain.Issue) error270+ // Events are appended atomically with the write, with sequence IDs assigned
271+ // by the store. Events beyond kvstore.MaxIssueEvents (500) are silently dropped.
272+ Update(ctx context.Context, issue domain.Issue, events ...domain.IssueEvent) error
273+ // ListEvents returns history oldest first.
274+ ListEvents(ctx context.Context, repo domain.RepoName, number int64) ([]domain.IssueEvent, error)
271 // SaveComment writes a comment and the issue it belongs to in one batch: a275 // SaveComment writes a comment and the issue it belongs to in one batch: a
272 // new comment (Version 0) must not exist yet, an existing one must be276 // new comment (Version 0) must not exist yet, an existing one must be
273 // unchanged since it was read, and so must issue. It returns277 // unchanged since it was read, and so must issue. It returns
@@ -548,7 +552,11 @@ type IssueLinkStore interface {
548 // ReplaceLinks atomically replaces an MR's links (closes is a subset of issues).552 // ReplaceLinks atomically replaces an MR's links (closes is a subset of issues).
549 // Added includes new links and links whose closes flag turned on.553 // Added includes new links and links whose closes flag turned on.
550 // Returns ErrNotFound for a missing repository, ErrConflict on a race.554 // Returns ErrNotFound for a missing repository, ErrConflict on a race.
551- ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64) (added, removed []domain.IssueLink, err error)555+ ReplaceLinks(ctx context.Context, repo domain.RepoName, mr int64, issues, closes []int64, ref ReferenceEvent) (added, removed []domain.IssueLink, err error)
556+ // EventsOfMergeRequest returns at most 500 events, oldest CreatedAt first,
557+ // breaking ties by issue number then sequence ID. Events can name issues
558+ // absent from the current links.
559+ EventsOfMergeRequest(ctx context.Context, repo domain.RepoName, mr int64) ([]domain.IssueEvent, error)
552 // LinksOfIssue returns links ascending by MR number, at most limit.560 // LinksOfIssue returns links ascending by MR number, at most limit.
553 LinksOfIssue(ctx context.Context, repo domain.RepoName, issue int64, limit int) ([]domain.IssueLink, error)561 LinksOfIssue(ctx context.Context, repo domain.RepoName, issue int64, limit int) ([]domain.IssueLink, error)
554 // LinksOfMergeRequest returns the issues named by an MR.562 // LinksOfMergeRequest returns the issues named by an MR.
@@ -562,8 +570,8 @@ type Clock interface {
562 570
563 // IssueLinkTrigger updates derived links without failing the primary operation.571 // IssueLinkTrigger updates derived links without failing the primary operation.
564 type IssueLinkTrigger interface {572 type IssueLinkTrigger interface {
565- OnMergeRequestChanged(context.Context, domain.Repo, int64)573+ OnMergeRequestChanged(context.Context, domain.Principal, domain.Repo, int64)
566 OnMergeRequestSettled(context.Context, domain.Repo, domain.MergeRequest)574 OnMergeRequestSettled(context.Context, domain.Repo, domain.MergeRequest)
567- OnBranchesMoved(context.Context, domain.RepoName, []domain.RefUpdate)575+ OnBranchesMoved(context.Context, domain.Principal, domain.RepoName, []domain.RefUpdate)
568 OnMerged(context.Context, domain.Principal, domain.Repo, domain.MergeRequest)576 OnMerged(context.Context, domain.Principal, domain.Repo, domain.MergeRequest)
569 }577 }
M internal/ports/services.go +1 -0
@@ -146,6 +146,7 @@ type IssueService interface {
146146 // Get returns the issue with its comments, oldest first.
147147 Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (IssueDetail, error)
148148 AddComment(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, text string) (domain.IssueComment, error)
149+ Resolve(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
149150 Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
150151 Reopen(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
151152 }
@@ -146,6 +146,7 @@ type IssueService interface {
146 // Get returns the issue with its comments, oldest first.146 // Get returns the issue with its comments, oldest first.
147 Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (IssueDetail, error)147 Get(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (IssueDetail, error)
148 AddComment(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, text string) (domain.IssueComment, error)148 AddComment(ctx context.Context, p domain.Principal, name domain.RepoName, number int64, text string) (domain.IssueComment, error)
149+ Resolve(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
149 Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)150 Close(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
150 Reopen(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)151 Reopen(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error)
151 }152 }
M internal/usecase/issue_links.go +9 -9
@@ -81,11 +81,11 @@ func NewIssueLinks(cfg IssueLinksConfig) *IssueLinks {
8181 }
8282
8383 // Reindex replaces links from the current texts and bounded commit range.
84-func (l *IssueLinks) Reindex(ctx context.Context, repo domain.Repo, number int64) error {
85- _, err := l.reindex(ctx, repo, number)
84+func (l *IssueLinks) Reindex(ctx context.Context, p domain.Principal, repo domain.Repo, number int64) error {
85+ _, err := l.reindex(ctx, p, repo, number)
8686 return err
8787 }
88-func (l *IssueLinks) reindex(ctx context.Context, repo domain.Repo, number int64) ([]int64, error) {
88+func (l *IssueLinks) reindex(ctx context.Context, p domain.Principal, repo domain.Repo, number int64) ([]int64, error) {
8989 unlock, err := l.lockMR(ctx, repo.Name, number)
9090 if err != nil {
9191 return nil, err
@@ -153,7 +153,7 @@ func (l *IssueLinks) reindex(ctx context.Context, repo domain.Repo, number int64
153153 }
154154 }
155155 for range maxWriteRetries {
156- added, removed, err := l.cfg.Links.ReplaceLinks(ctx, repo.Name, number, mentions, closes)
156+ added, removed, err := l.cfg.Links.ReplaceLinks(ctx, repo.Name, number, mentions, closes, ports.ReferenceEvent{})
157157 if errors.Is(err, domain.ErrConflict) {
158158 continue
159159 }
@@ -186,10 +186,10 @@ func linkWarning(ctx context.Context, err error) {
186186 log.Warn().Err(err).Msg("issue links not updated")
187187 }
188188 }
189-func (l *IssueLinks) OnMergeRequestChanged(ctx context.Context, repo domain.Repo, number int64) {
189+func (l *IssueLinks) OnMergeRequestChanged(ctx context.Context, p domain.Principal, repo domain.Repo, number int64) {
190190 ctx, cancel := linkContext(ctx)
191191 defer cancel()
192- linkWarning(ctx, l.Reindex(ctx, repo, number))
192+ linkWarning(ctx, l.Reindex(ctx, p, repo, number))
193193 }
194194 func (l *IssueLinks) settled(ctx context.Context, repo domain.Repo, mr domain.MergeRequest) {
195195 links, err := l.cfg.Links.LinksOfMergeRequest(ctx, repo.Name, mr.Number)
@@ -205,7 +205,7 @@ func (l *IssueLinks) OnMergeRequestSettled(ctx context.Context, repo domain.Repo
205205 defer cancel()
206206 l.settled(ctx, repo, mr)
207207 }
208-func (l *IssueLinks) OnBranchesMoved(ctx context.Context, name domain.RepoName, updates []domain.RefUpdate) {
208+func (l *IssueLinks) OnBranchesMoved(ctx context.Context, p domain.Principal, name domain.RepoName, updates []domain.RefUpdate) {
209209 ctx, cancel := linkContext(ctx)
210210 defer cancel()
211211 var branches []string
@@ -232,14 +232,14 @@ func (l *IssueLinks) OnBranchesMoved(ctx context.Context, name domain.RepoName,
232232 log.Warn().Str("repo", name.String()).Msg("issue link merge requests truncated")
233233 }
234234 for _, n := range numbers {
235- linkWarning(ctx, l.Reindex(ctx, repo, n))
235+ linkWarning(ctx, l.Reindex(ctx, p, repo, n))
236236 }
237237 }
238238 func (l *IssueLinks) OnMerged(ctx context.Context, p domain.Principal, repo domain.Repo, mr domain.MergeRequest) {
239239 ctx, cancel := linkContext(ctx)
240240 defer cancel()
241241 defer l.settled(ctx, repo, mr)
242- closes, err := l.reindex(ctx, repo, mr.Number)
242+ closes, err := l.reindex(ctx, p, repo, mr.Number)
243243 if err != nil {
244244 linkWarning(ctx, err)
245245 return
@@ -81,11 +81,11 @@ func NewIssueLinks(cfg IssueLinksConfig) *IssueLinks {
81 }81 }
82 82
83 // Reindex replaces links from the current texts and bounded commit range.83 // Reindex replaces links from the current texts and bounded commit range.
84-func (l *IssueLinks) Reindex(ctx context.Context, repo domain.Repo, number int64) error {84+func (l *IssueLinks) Reindex(ctx context.Context, p domain.Principal, repo domain.Repo, number int64) error {
85- _, err := l.reindex(ctx, repo, number)85+ _, err := l.reindex(ctx, p, repo, number)
86 return err86 return err
87 }87 }
88-func (l *IssueLinks) reindex(ctx context.Context, repo domain.Repo, number int64) ([]int64, error) {88+func (l *IssueLinks) reindex(ctx context.Context, p domain.Principal, repo domain.Repo, number int64) ([]int64, error) {
89 unlock, err := l.lockMR(ctx, repo.Name, number)89 unlock, err := l.lockMR(ctx, repo.Name, number)
90 if err != nil {90 if err != nil {
91 return nil, err91 return nil, err
@@ -153,7 +153,7 @@ func (l *IssueLinks) reindex(ctx context.Context, repo domain.Repo, number int64
153 }153 }
154 }154 }
155 for range maxWriteRetries {155 for range maxWriteRetries {
156- added, removed, err := l.cfg.Links.ReplaceLinks(ctx, repo.Name, number, mentions, closes)156+ added, removed, err := l.cfg.Links.ReplaceLinks(ctx, repo.Name, number, mentions, closes, ports.ReferenceEvent{})
157 if errors.Is(err, domain.ErrConflict) {157 if errors.Is(err, domain.ErrConflict) {
158 continue158 continue
159 }159 }
@@ -186,10 +186,10 @@ func linkWarning(ctx context.Context, err error) {
186 log.Warn().Err(err).Msg("issue links not updated")186 log.Warn().Err(err).Msg("issue links not updated")
187 }187 }
188 }188 }
189-func (l *IssueLinks) OnMergeRequestChanged(ctx context.Context, repo domain.Repo, number int64) {189+func (l *IssueLinks) OnMergeRequestChanged(ctx context.Context, p domain.Principal, repo domain.Repo, number int64) {
190 ctx, cancel := linkContext(ctx)190 ctx, cancel := linkContext(ctx)
191 defer cancel()191 defer cancel()
192- linkWarning(ctx, l.Reindex(ctx, repo, number))192+ linkWarning(ctx, l.Reindex(ctx, p, repo, number))
193 }193 }
194 func (l *IssueLinks) settled(ctx context.Context, repo domain.Repo, mr domain.MergeRequest) {194 func (l *IssueLinks) settled(ctx context.Context, repo domain.Repo, mr domain.MergeRequest) {
195 links, err := l.cfg.Links.LinksOfMergeRequest(ctx, repo.Name, mr.Number)195 links, err := l.cfg.Links.LinksOfMergeRequest(ctx, repo.Name, mr.Number)
@@ -205,7 +205,7 @@ func (l *IssueLinks) OnMergeRequestSettled(ctx context.Context, repo domain.Repo
205 defer cancel()205 defer cancel()
206 l.settled(ctx, repo, mr)206 l.settled(ctx, repo, mr)
207 }207 }
208-func (l *IssueLinks) OnBranchesMoved(ctx context.Context, name domain.RepoName, updates []domain.RefUpdate) {208+func (l *IssueLinks) OnBranchesMoved(ctx context.Context, p domain.Principal, name domain.RepoName, updates []domain.RefUpdate) {
209 ctx, cancel := linkContext(ctx)209 ctx, cancel := linkContext(ctx)
210 defer cancel()210 defer cancel()
211 var branches []string211 var branches []string
@@ -232,14 +232,14 @@ func (l *IssueLinks) OnBranchesMoved(ctx context.Context, name domain.RepoName,
232 log.Warn().Str("repo", name.String()).Msg("issue link merge requests truncated")232 log.Warn().Str("repo", name.String()).Msg("issue link merge requests truncated")
233 }233 }
234 for _, n := range numbers {234 for _, n := range numbers {
235- linkWarning(ctx, l.Reindex(ctx, repo, n))235+ linkWarning(ctx, l.Reindex(ctx, p, repo, n))
236 }236 }
237 }237 }
238 func (l *IssueLinks) OnMerged(ctx context.Context, p domain.Principal, repo domain.Repo, mr domain.MergeRequest) {238 func (l *IssueLinks) OnMerged(ctx context.Context, p domain.Principal, repo domain.Repo, mr domain.MergeRequest) {
239 ctx, cancel := linkContext(ctx)239 ctx, cancel := linkContext(ctx)
240 defer cancel()240 defer cancel()
241 defer l.settled(ctx, repo, mr)241 defer l.settled(ctx, repo, mr)
242- closes, err := l.reindex(ctx, repo, mr.Number)242+ closes, err := l.reindex(ctx, p, repo, mr.Number)
243 if err != nil {243 if err != nil {
244 linkWarning(ctx, err)244 linkWarning(ctx, err)
245 return245 return
M internal/usecase/issue_links_test.go +5 -5
@@ -59,7 +59,7 @@ func TestIssueLinksTextsPushAndReferences(t *testing.T) {
5959 assert.Empty(t, got.References)
6060 head := r.commit(author, "push.txt", "push", fmt.Sprintf("Fixes #%d", other.Number))
6161 r.push("feat")
62- links.OnBranchesMoved(r.ctx, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})
62+ links.OnBranchesMoved(r.ctx, domain.Principal{}, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})
6363 got, err = issues.Get(r.ctx, root, r.repo, other.Number)
6464 require.NoError(t, err)
6565 require.Len(t, got.References, 1)
@@ -161,12 +161,12 @@ func TestIssueLinksReindexSerializedPerMergeRequest(t *testing.T) {
161161 repo, err := r.store.Get(r.ctx, r.repo)
162162 require.NoError(t, err)
163163 done := make(chan error, 2)
164- go func() { done <- links.Reindex(r.ctx, repo, mr.Number) }()
164+ go func() { done <- links.Reindex(r.ctx, domain.Principal{}, repo, mr.Number) }()
165165 <-g.started
166166 // The first reindex read the old texts; a note is added afterwards.
167167 _, err = r.mrs.AddComment(r.ctx, root, r.repo, mr.Number, ports.AddCommentParams{Text: fmt.Sprintf("see #%d", issue.Number)})
168168 require.NoError(t, err)
169- go func() { done <- links.Reindex(r.ctx, repo, mr.Number) }()
169+ go func() { done <- links.Reindex(r.ctx, domain.Principal{}, repo, mr.Number) }()
170170 time.Sleep(100 * time.Millisecond)
171171 assert.EqualValues(t, 1, g.entered.Load(), "the second reindex waits for the first")
172172 close(g.gate)
@@ -202,11 +202,11 @@ func TestIssueLinksNotifiesRemovalsAndClosingFlips(t *testing.T) {
202202 // then is rewritten to a plain mention.
203203 head := r.commit(author, "fix.txt", "fix", fmt.Sprintf("Fixes #%d", issue.Number))
204204 r.push("feat")
205- links.OnBranchesMoved(r.ctx, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})
205+ links.OnBranchesMoved(r.ctx, domain.Principal{}, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})
206206 referenced()
207207 r.sh("commit", "-q", "--amend", "-m", fmt.Sprintf("See #%d", issue.Number))
208208 r.push("feat")
209- links.OnBranchesMoved(r.ctx, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: r.branchHead("feat").String()}})
209+ links.OnBranchesMoved(r.ctx, domain.Principal{}, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: r.branchHead("feat").String()}})
210210 assert.Equal(t, 1, referenced(), "a closing flip notifies once")
211211 }
212212
@@ -59,7 +59,7 @@ func TestIssueLinksTextsPushAndReferences(t *testing.T) {
59 assert.Empty(t, got.References)59 assert.Empty(t, got.References)
60 head := r.commit(author, "push.txt", "push", fmt.Sprintf("Fixes #%d", other.Number))60 head := r.commit(author, "push.txt", "push", fmt.Sprintf("Fixes #%d", other.Number))
61 r.push("feat")61 r.push("feat")
62- links.OnBranchesMoved(r.ctx, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})62+ links.OnBranchesMoved(r.ctx, domain.Principal{}, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})
63 got, err = issues.Get(r.ctx, root, r.repo, other.Number)63 got, err = issues.Get(r.ctx, root, r.repo, other.Number)
64 require.NoError(t, err)64 require.NoError(t, err)
65 require.Len(t, got.References, 1)65 require.Len(t, got.References, 1)
@@ -161,12 +161,12 @@ func TestIssueLinksReindexSerializedPerMergeRequest(t *testing.T) {
161 repo, err := r.store.Get(r.ctx, r.repo)161 repo, err := r.store.Get(r.ctx, r.repo)
162 require.NoError(t, err)162 require.NoError(t, err)
163 done := make(chan error, 2)163 done := make(chan error, 2)
164- go func() { done <- links.Reindex(r.ctx, repo, mr.Number) }()164+ go func() { done <- links.Reindex(r.ctx, domain.Principal{}, repo, mr.Number) }()
165 <-g.started165 <-g.started
166 // The first reindex read the old texts; a note is added afterwards.166 // The first reindex read the old texts; a note is added afterwards.
167 _, err = r.mrs.AddComment(r.ctx, root, r.repo, mr.Number, ports.AddCommentParams{Text: fmt.Sprintf("see #%d", issue.Number)})167 _, err = r.mrs.AddComment(r.ctx, root, r.repo, mr.Number, ports.AddCommentParams{Text: fmt.Sprintf("see #%d", issue.Number)})
168 require.NoError(t, err)168 require.NoError(t, err)
169- go func() { done <- links.Reindex(r.ctx, repo, mr.Number) }()169+ go func() { done <- links.Reindex(r.ctx, domain.Principal{}, repo, mr.Number) }()
170 time.Sleep(100 * time.Millisecond)170 time.Sleep(100 * time.Millisecond)
171 assert.EqualValues(t, 1, g.entered.Load(), "the second reindex waits for the first")171 assert.EqualValues(t, 1, g.entered.Load(), "the second reindex waits for the first")
172 close(g.gate)172 close(g.gate)
@@ -202,11 +202,11 @@ func TestIssueLinksNotifiesRemovalsAndClosingFlips(t *testing.T) {
202 // then is rewritten to a plain mention.202 // then is rewritten to a plain mention.
203 head := r.commit(author, "fix.txt", "fix", fmt.Sprintf("Fixes #%d", issue.Number))203 head := r.commit(author, "fix.txt", "fix", fmt.Sprintf("Fixes #%d", issue.Number))
204 r.push("feat")204 r.push("feat")
205- links.OnBranchesMoved(r.ctx, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})205+ links.OnBranchesMoved(r.ctx, domain.Principal{}, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: head.String()}})
206 referenced()206 referenced()
207 r.sh("commit", "-q", "--amend", "-m", fmt.Sprintf("See #%d", issue.Number))207 r.sh("commit", "-q", "--amend", "-m", fmt.Sprintf("See #%d", issue.Number))
208 r.push("feat")208 r.push("feat")
209- links.OnBranchesMoved(r.ctx, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: r.branchHead("feat").String()}})209+ links.OnBranchesMoved(r.ctx, domain.Principal{}, r.repo, []domain.RefUpdate{{Ref: "refs/heads/feat", New: r.branchHead("feat").String()}})
210 assert.Equal(t, 1, referenced(), "a closing flip notifies once")210 assert.Equal(t, 1, referenced(), "a closing flip notifies once")
211 }211 }
212 212
M internal/usecase/issues.go +5 -0
@@ -318,3 +318,8 @@ func (i *Issues) Kind(ctx context.Context, p domain.Principal, name domain.RepoN
318318 }
319319 return domain.NumberNone, nil
320320 }
321+
322+// Resolve is reserved for Phase A; no state is changed by this contract stub.
323+func (i *Issues) Resolve(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error) {
324+ return domain.Issue{}, domain.ErrUnavailable
325+}
@@ -318,3 +318,8 @@ func (i *Issues) Kind(ctx context.Context, p domain.Principal, name domain.RepoN
318 }318 }
319 return domain.NumberNone, nil319 return domain.NumberNone, nil
320 }320 }
321+
322+// Resolve is reserved for Phase A; no state is changed by this contract stub.
323+func (i *Issues) Resolve(ctx context.Context, p domain.Principal, name domain.RepoName, number int64) (domain.Issue, error) {
324+ return domain.Issue{}, domain.ErrUnavailable
325+}
M internal/usecase/mergerequests.go +5 -5
@@ -295,7 +295,7 @@ func (m *MergeRequests) Create(ctx context.Context, p domain.Principal, name dom
295295 recordActivity(ctx, m.meta, repo.Name, now)
296296 m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
297297 if m.links != nil {
298- m.links.OnMergeRequestChanged(ctx, repo, mr.Number)
298+ m.links.OnMergeRequestChanged(ctx, p, repo, mr.Number)
299299 }
300300 if m.mergeab != nil {
301301 m.mergeab.OnOpened(ctx, mr)
@@ -440,7 +440,7 @@ func (m *MergeRequests) MarkReady(ctx context.Context, p domain.Principal, name
440440 recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt)
441441 m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
442442 if m.links != nil {
443- m.links.OnMergeRequestChanged(ctx, repo, number)
443+ m.links.OnMergeRequestChanged(ctx, p, repo, number)
444444 }
445445 return m.withHead(ctx, mr)
446446 }
@@ -797,7 +797,7 @@ func (m *MergeRequests) AddComment(ctx context.Context, p domain.Principal, name
797797 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,
798798 })
799799 if m.links != nil {
800- m.links.OnMergeRequestChanged(ctx, repo, number)
800+ m.links.OnMergeRequestChanged(ctx, p, repo, number)
801801 }
802802 return c, nil
803803 }
@@ -1276,7 +1276,7 @@ func (m *MergeRequests) afterMerge(ctx context.Context, p domain.Principal, repo
12761276 m.mergeab.OnBranchesMoved(ctx, name, updates) // the other requests of the moved target
12771277 }
12781278 if m.links != nil {
1279- m.links.OnBranchesMoved(ctx, name, updates) // open requests whose source moved
1279+ m.links.OnBranchesMoved(ctx, p, name, updates) // open requests whose source moved
12801280 }
12811281 }
12821282
@@ -1553,7 +1553,7 @@ func (m *MergeRequests) changeComment(ctx context.Context, p domain.Principal, n
15531553 m.bus.Publish(ctx, domain.CommentEdited{Repo: name, Number: number, Comment: c, At: c.EditedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
15541554 }
15551555 if m.links != nil {
1556- m.links.OnMergeRequestChanged(ctx, repo, number)
1556+ m.links.OnMergeRequestChanged(ctx, p, repo, number)
15571557 }
15581558 return c, nil
15591559 }
@@ -295,7 +295,7 @@ func (m *MergeRequests) Create(ctx context.Context, p domain.Principal, name dom
295 recordActivity(ctx, m.meta, repo.Name, now)295 recordActivity(ctx, m.meta, repo.Name, now)
296 m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})296 m.bus.Publish(ctx, domain.MergeRequestOpened{MergeRequest: mr, At: now, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
297 if m.links != nil {297 if m.links != nil {
298- m.links.OnMergeRequestChanged(ctx, repo, mr.Number)298+ m.links.OnMergeRequestChanged(ctx, p, repo, mr.Number)
299 }299 }
300 if m.mergeab != nil {300 if m.mergeab != nil {
301 m.mergeab.OnOpened(ctx, mr)301 m.mergeab.OnOpened(ctx, mr)
@@ -440,7 +440,7 @@ func (m *MergeRequests) MarkReady(ctx context.Context, p domain.Principal, name
440 recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt)440 recordActivity(ctx, m.meta, repo.Name, mr.UpdatedAt)
441 m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})441 m.bus.Publish(ctx, domain.MergeRequestReady{MergeRequest: mr, At: mr.UpdatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
442 if m.links != nil {442 if m.links != nil {
443- m.links.OnMergeRequestChanged(ctx, repo, number)443+ m.links.OnMergeRequestChanged(ctx, p, repo, number)
444 }444 }
445 return m.withHead(ctx, mr)445 return m.withHead(ctx, mr)
446 }446 }
@@ -797,7 +797,7 @@ func (m *MergeRequests) AddComment(ctx context.Context, p domain.Principal, name
797 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,797 Repo: name, Number: number, Comment: c, At: c.CreatedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt,
798 })798 })
799 if m.links != nil {799 if m.links != nil {
800- m.links.OnMergeRequestChanged(ctx, repo, number)800+ m.links.OnMergeRequestChanged(ctx, p, repo, number)
801 }801 }
802 return c, nil802 return c, nil
803 }803 }
@@ -1276,7 +1276,7 @@ func (m *MergeRequests) afterMerge(ctx context.Context, p domain.Principal, repo
1276 m.mergeab.OnBranchesMoved(ctx, name, updates) // the other requests of the moved target1276 m.mergeab.OnBranchesMoved(ctx, name, updates) // the other requests of the moved target
1277 }1277 }
1278 if m.links != nil {1278 if m.links != nil {
1279- m.links.OnBranchesMoved(ctx, name, updates) // open requests whose source moved1279+ m.links.OnBranchesMoved(ctx, p, name, updates) // open requests whose source moved
1280 }1280 }
1281 }1281 }
1282 1282
@@ -1553,7 +1553,7 @@ func (m *MergeRequests) changeComment(ctx context.Context, p domain.Principal, n
1553 m.bus.Publish(ctx, domain.CommentEdited{Repo: name, Number: number, Comment: c, At: c.EditedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})1553 m.bus.Publish(ctx, domain.CommentEdited{Repo: name, Number: number, Comment: c, At: c.EditedAt, Visibility: repo.Visibility, CreatedAt: repo.CreatedAt})
1554 }1554 }
1555 if m.links != nil {1555 if m.links != nil {
1556- m.links.OnMergeRequestChanged(ctx, repo, number)1556+ m.links.OnMergeRequestChanged(ctx, p, repo, number)
1557 }1557 }
1558 return c, nil1558 return c, nil
1559 }1559 }
M internal/usecase/pushes.go +1 -1
@@ -274,7 +274,7 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep
274274 u.mergeab.OnBranchesMoved(ctx, name, updates) // queues only
275275 }
276276 if u.links != nil {
277- u.links.OnBranchesMoved(ctx, name, updates)
277+ u.links.OnBranchesMoved(ctx, p, name, updates)
278278 }
279279 if u.releases != nil {
280280 u.releases.OnRefsMoved(ctx, name, updates)
@@ -274,7 +274,7 @@ func (u *Pushes) Record(ctx context.Context, p domain.Principal, name domain.Rep
274 u.mergeab.OnBranchesMoved(ctx, name, updates) // queues only274 u.mergeab.OnBranchesMoved(ctx, name, updates) // queues only
275 }275 }
276 if u.links != nil {276 if u.links != nil {
277- u.links.OnBranchesMoved(ctx, name, updates)277+ u.links.OnBranchesMoved(ctx, p, name, updates)
278 }278 }
279 if u.releases != nil {279 if u.releases != nil {
280 u.releases.OnRefsMoved(ctx, name, updates)280 u.releases.OnRefsMoved(ctx, name, updates)
M proto/ermite/storage/issue.proto +29 -0
@@ -17,6 +17,7 @@ enum IssueState {
1717 ISSUE_STATE_CLOSED = 2;
1818 // Awaiting moderation. Not stored yet.
1919 ISSUE_STATE_PENDING = 3;
20+ ISSUE_STATE_RESOLVED = 4;
2021 }
2122
2223 // Issue is stored under the key "is/<repo>/<number:010d>". The numbers come
@@ -36,6 +37,7 @@ message Issue {
3637 // True for an issue approved from the public form: its text comes from
3738 // an anonymous visitor and is rendered as untrusted.
3839 bool external = 9;
40+ // Set only while RESOLVED by a merge.
3941 int64 closed_by_merge_request = 10;
4042 }
4143
@@ -81,3 +83,30 @@ message IssueBan {
8183 message IssueKey {
8284 bytes key = 1;
8385 }
86+
87+// IssueEventKind describes an issue history entry.
88+enum IssueEventKind {
89+ ISSUE_EVENT_KIND_UNSPECIFIED = 0;
90+ // Closes says whether this is a closing reference.
91+ ISSUE_EVENT_KIND_REFERENCED = 1;
92+ ISSUE_EVENT_KIND_CLOSED = 2;
93+ ISSUE_EVENT_KIND_REOPENED = 3;
94+ // Merge_request is set when a merge resolved the issue.
95+ ISSUE_EVENT_KIND_RESOLVED = 4;
96+}
97+
98+// IssueEvent is stored under "isev/<repo>/<issue:010d>/<seq:010d>" and,
99+// when merge_request != 0, copied verbatim under
100+// "mrev/<repo>/<mr:010d>/<issue:010d>/<seq:010d>". Append-only: never
101+// updated or deleted except with the repository.
102+message IssueEvent {
103+ int64 seq = 1;
104+ IssueEventKind kind = 2;
105+ // Zero if none.
106+ int64 merge_request = 3;
107+ // REFERENCED only.
108+ bool closes = 4;
109+ // Label, like Issue.author.
110+ string actor = 5;
111+ google.protobuf.Timestamp created_at = 6;
112+}
@@ -17,6 +17,7 @@ enum IssueState {
17 ISSUE_STATE_CLOSED = 2;17 ISSUE_STATE_CLOSED = 2;
18 // Awaiting moderation. Not stored yet.18 // Awaiting moderation. Not stored yet.
19 ISSUE_STATE_PENDING = 3;19 ISSUE_STATE_PENDING = 3;
20+ ISSUE_STATE_RESOLVED = 4;
20 }21 }
21 22
22 // Issue is stored under the key "is/<repo>/<number:010d>". The numbers come23 // Issue is stored under the key "is/<repo>/<number:010d>". The numbers come
@@ -36,6 +37,7 @@ message Issue {
36 // True for an issue approved from the public form: its text comes from37 // True for an issue approved from the public form: its text comes from
37 // an anonymous visitor and is rendered as untrusted.38 // an anonymous visitor and is rendered as untrusted.
38 bool external = 9;39 bool external = 9;
40+ // Set only while RESOLVED by a merge.
39 int64 closed_by_merge_request = 10;41 int64 closed_by_merge_request = 10;
40 }42 }
41 43
@@ -81,3 +83,30 @@ message IssueBan {
81 message IssueKey {83 message IssueKey {
82 bytes key = 1;84 bytes key = 1;
83 }85 }
86+
87+// IssueEventKind describes an issue history entry.
88+enum IssueEventKind {
89+ ISSUE_EVENT_KIND_UNSPECIFIED = 0;
90+ // Closes says whether this is a closing reference.
91+ ISSUE_EVENT_KIND_REFERENCED = 1;
92+ ISSUE_EVENT_KIND_CLOSED = 2;
93+ ISSUE_EVENT_KIND_REOPENED = 3;
94+ // Merge_request is set when a merge resolved the issue.
95+ ISSUE_EVENT_KIND_RESOLVED = 4;
96+}
97+
98+// IssueEvent is stored under "isev/<repo>/<issue:010d>/<seq:010d>" and,
99+// when merge_request != 0, copied verbatim under
100+// "mrev/<repo>/<mr:010d>/<issue:010d>/<seq:010d>". Append-only: never
101+// updated or deleted except with the repository.
102+message IssueEvent {
103+ int64 seq = 1;
104+ IssueEventKind kind = 2;
105+ // Zero if none.
106+ int64 merge_request = 3;
107+ // REFERENCED only.
108+ bool closes = 4;
109+ // Label, like Issue.author.
110+ string actor = 5;
111+ google.protobuf.Timestamp created_at = 6;
112+}

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 4

Tags 45

↑↓ moveenter openesc close