feat(kvstore): store merge requests, repository settings and the signing key
Keys mr/<repo>/<n:010d>, mrc/<repo>/<n>/<id>, mrread/<repo>/<n>, mrseq/<repo> (next number taken under RequireVersion) and signing/key. Repository settings are stored explicitly with the repository; one stored before merge requests existed reads as the defaults. Deleting a repository drops its merge requests, comments, read positions and sequence with it: like the repository metadata they are not kept in the trash, and a repository created again under the same name starts at !1.
- commit
- 858a2fbc9b3c9996e06532bc81e614ecdf6a9469
- author
- brice · · 2026-10-10 16:49 UTC
- parent
- b60fb76
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
11 files · +1983 -15
A gen/ermite/storage/merge_request.pb.go +664 -0
| @@ -0,0 +1,664 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | // Internal, unversioned storage schema of ermite: merge requests. | |
| 4 | + | ||
| 5 | + | // Code generated by protoc-gen-go. DO NOT EDIT. | |
| 6 | + | // versions: | |
| 7 | + | // protoc-gen-go v1.36.12 | |
| 8 | + | // protoc (unknown) | |
| 9 | + | // source: ermite/storage/merge_request.proto | |
| 10 | + | ||
| 11 | + | package storagepb | |
| 12 | + | ||
| 13 | + | import ( | |
| 14 | + | protoreflect "google.golang.org/protobuf/reflect/protoreflect" | |
| 15 | + | protoimpl "google.golang.org/protobuf/runtime/protoimpl" | |
| 16 | + | timestamppb "google.golang.org/protobuf/types/known/timestamppb" | |
| 17 | + | reflect "reflect" | |
| 18 | + | sync "sync" | |
| 19 | + | unsafe "unsafe" | |
| 20 | + | ) | |
| 21 | + | ||
| 22 | + | const ( | |
| 23 | + | // Verify that this generated code is sufficiently up-to-date. | |
| 24 | + | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) | |
| 25 | + | // Verify that runtime/protoimpl is sufficiently up-to-date. | |
| 26 | + | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) | |
| 27 | + | ) | |
| 28 | + | ||
| 29 | + | // MergeMode is how a merge request is merged. | |
| 30 | + | type MergeMode int32 | |
| 31 | + | ||
| 32 | + | const ( | |
| 33 | + | MergeMode_MERGE_MODE_UNSPECIFIED MergeMode = 0 | |
| 34 | + | MergeMode_MERGE_MODE_FAST_FORWARD MergeMode = 1 | |
| 35 | + | MergeMode_MERGE_MODE_SQUASH MergeMode = 2 | |
| 36 | + | ) | |
| 37 | + | ||
| 38 | + | // Enum value maps for MergeMode. | |
| 39 | + | var ( | |
| 40 | + | MergeMode_name = map[int32]string{ | |
| 41 | + | 0: "MERGE_MODE_UNSPECIFIED", | |
| 42 | + | 1: "MERGE_MODE_FAST_FORWARD", | |
| 43 | + | 2: "MERGE_MODE_SQUASH", | |
| 44 | + | } | |
| 45 | + | MergeMode_value = map[string]int32{ | |
| 46 | + | "MERGE_MODE_UNSPECIFIED": 0, | |
| 47 | + | "MERGE_MODE_FAST_FORWARD": 1, | |
| 48 | + | "MERGE_MODE_SQUASH": 2, | |
| 49 | + | } | |
| 50 | + | ) | |
| 51 | + | ||
| 52 | + | func (x MergeMode) Enum() *MergeMode { | |
| 53 | + | p := new(MergeMode) | |
| 54 | + | *p = x | |
| 55 | + | return p | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | func (x MergeMode) String() string { | |
| 59 | + | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func (MergeMode) Descriptor() protoreflect.EnumDescriptor { | |
| 63 | + | return file_ermite_storage_merge_request_proto_enumTypes[0].Descriptor() | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (MergeMode) Type() protoreflect.EnumType { | |
| 67 | + | return &file_ermite_storage_merge_request_proto_enumTypes[0] | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | func (x MergeMode) Number() protoreflect.EnumNumber { | |
| 71 | + | return protoreflect.EnumNumber(x) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // Deprecated: Use MergeMode.Descriptor instead. | |
| 75 | + | func (MergeMode) EnumDescriptor() ([]byte, []int) { | |
| 76 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{0} | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // MergeRequestState is where a merge request is in its life. | |
| 80 | + | type MergeRequestState int32 | |
| 81 | + | ||
| 82 | + | const ( | |
| 83 | + | MergeRequestState_MERGE_REQUEST_STATE_UNSPECIFIED MergeRequestState = 0 | |
| 84 | + | MergeRequestState_MERGE_REQUEST_STATE_OPEN MergeRequestState = 1 | |
| 85 | + | MergeRequestState_MERGE_REQUEST_STATE_MERGED MergeRequestState = 2 | |
| 86 | + | MergeRequestState_MERGE_REQUEST_STATE_CLOSED MergeRequestState = 3 | |
| 87 | + | ) | |
| 88 | + | ||
| 89 | + | // Enum value maps for MergeRequestState. | |
| 90 | + | var ( | |
| 91 | + | MergeRequestState_name = map[int32]string{ | |
| 92 | + | 0: "MERGE_REQUEST_STATE_UNSPECIFIED", | |
| 93 | + | 1: "MERGE_REQUEST_STATE_OPEN", | |
| 94 | + | 2: "MERGE_REQUEST_STATE_MERGED", | |
| 95 | + | 3: "MERGE_REQUEST_STATE_CLOSED", | |
| 96 | + | } | |
| 97 | + | MergeRequestState_value = map[string]int32{ | |
| 98 | + | "MERGE_REQUEST_STATE_UNSPECIFIED": 0, | |
| 99 | + | "MERGE_REQUEST_STATE_OPEN": 1, | |
| 100 | + | "MERGE_REQUEST_STATE_MERGED": 2, | |
| 101 | + | "MERGE_REQUEST_STATE_CLOSED": 3, | |
| 102 | + | } | |
| 103 | + | ) | |
| 104 | + | ||
| 105 | + | func (x MergeRequestState) Enum() *MergeRequestState { | |
| 106 | + | p := new(MergeRequestState) | |
| 107 | + | *p = x | |
| 108 | + | return p | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func (x MergeRequestState) String() string { | |
| 112 | + | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func (MergeRequestState) Descriptor() protoreflect.EnumDescriptor { | |
| 116 | + | return file_ermite_storage_merge_request_proto_enumTypes[1].Descriptor() | |
| 117 | + | } | |
| 118 | + | ||
| 119 | + | func (MergeRequestState) Type() protoreflect.EnumType { | |
| 120 | + | return &file_ermite_storage_merge_request_proto_enumTypes[1] | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func (x MergeRequestState) Number() protoreflect.EnumNumber { | |
| 124 | + | return protoreflect.EnumNumber(x) | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | // Deprecated: Use MergeRequestState.Descriptor instead. | |
| 128 | + | func (MergeRequestState) EnumDescriptor() ([]byte, []int) { | |
| 129 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{1} | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // DiffSide is the side of a diff a comment is attached to. | |
| 133 | + | type DiffSide int32 | |
| 134 | + | ||
| 135 | + | const ( | |
| 136 | + | // A general comment. | |
| 137 | + | DiffSide_DIFF_SIDE_UNSPECIFIED DiffSide = 0 | |
| 138 | + | DiffSide_DIFF_SIDE_OLD DiffSide = 1 | |
| 139 | + | DiffSide_DIFF_SIDE_NEW DiffSide = 2 | |
| 140 | + | ) | |
| 141 | + | ||
| 142 | + | // Enum value maps for DiffSide. | |
| 143 | + | var ( | |
| 144 | + | DiffSide_name = map[int32]string{ | |
| 145 | + | 0: "DIFF_SIDE_UNSPECIFIED", | |
| 146 | + | 1: "DIFF_SIDE_OLD", | |
| 147 | + | 2: "DIFF_SIDE_NEW", | |
| 148 | + | } | |
| 149 | + | DiffSide_value = map[string]int32{ | |
| 150 | + | "DIFF_SIDE_UNSPECIFIED": 0, | |
| 151 | + | "DIFF_SIDE_OLD": 1, | |
| 152 | + | "DIFF_SIDE_NEW": 2, | |
| 153 | + | } | |
| 154 | + | ) | |
| 155 | + | ||
| 156 | + | func (x DiffSide) Enum() *DiffSide { | |
| 157 | + | p := new(DiffSide) | |
| 158 | + | *p = x | |
| 159 | + | return p | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | func (x DiffSide) String() string { | |
| 163 | + | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | func (DiffSide) Descriptor() protoreflect.EnumDescriptor { | |
| 167 | + | return file_ermite_storage_merge_request_proto_enumTypes[2].Descriptor() | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | func (DiffSide) Type() protoreflect.EnumType { | |
| 171 | + | return &file_ermite_storage_merge_request_proto_enumTypes[2] | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | func (x DiffSide) Number() protoreflect.EnumNumber { | |
| 175 | + | return protoreflect.EnumNumber(x) | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | // Deprecated: Use DiffSide.Descriptor instead. | |
| 179 | + | func (DiffSide) EnumDescriptor() ([]byte, []int) { | |
| 180 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{2} | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | // RepoSettings are the merge settings of a repository, stored inside Repo. | |
| 184 | + | type RepoSettings struct { | |
| 185 | + | state protoimpl.MessageState `protogen:"open.v1"` | |
| 186 | + | // Never empty once stored. | |
| 187 | + | AllowedMergeModes []MergeMode `protobuf:"varint,1,rep,packed,name=allowed_merge_modes,json=allowedMergeModes,proto3,enum=ermite.storage.MergeMode" json:"allowed_merge_modes,omitempty"` | |
| 188 | + | FailingChecksBlock bool `protobuf:"varint,2,opt,name=failing_checks_block,json=failingChecksBlock,proto3" json:"failing_checks_block,omitempty"` | |
| 189 | + | unknownFields protoimpl.UnknownFields | |
| 190 | + | sizeCache protoimpl.SizeCache | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | func (x *RepoSettings) Reset() { | |
| 194 | + | *x = RepoSettings{} | |
| 195 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[0] | |
| 196 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 197 | + | ms.StoreMessageInfo(mi) | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | func (x *RepoSettings) String() string { | |
| 201 | + | return protoimpl.X.MessageStringOf(x) | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | func (*RepoSettings) ProtoMessage() {} | |
| 205 | + | ||
| 206 | + | func (x *RepoSettings) ProtoReflect() protoreflect.Message { | |
| 207 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[0] | |
| 208 | + | if x != nil { | |
| 209 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 210 | + | if ms.LoadMessageInfo() == nil { | |
| 211 | + | ms.StoreMessageInfo(mi) | |
| 212 | + | } | |
| 213 | + | return ms | |
| 214 | + | } | |
| 215 | + | return mi.MessageOf(x) | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | // Deprecated: Use RepoSettings.ProtoReflect.Descriptor instead. | |
| 219 | + | func (*RepoSettings) Descriptor() ([]byte, []int) { | |
| 220 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{0} | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | func (x *RepoSettings) GetAllowedMergeModes() []MergeMode { | |
| 224 | + | if x != nil { | |
| 225 | + | return x.AllowedMergeModes | |
| 226 | + | } | |
| 227 | + | return nil | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | func (x *RepoSettings) GetFailingChecksBlock() bool { | |
| 231 | + | if x != nil { | |
| 232 | + | return x.FailingChecksBlock | |
| 233 | + | } | |
| 234 | + | return false | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | // MergeRequest is stored under the key "mr/<repo>/<number:010d>". | |
| 238 | + | type MergeRequest struct { | |
| 239 | + | state protoimpl.MessageState `protogen:"open.v1"` | |
| 240 | + | Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` | |
| 241 | + | Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` | |
| 242 | + | Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` | |
| 243 | + | SourceBranch string `protobuf:"bytes,4,opt,name=source_branch,json=sourceBranch,proto3" json:"source_branch,omitempty"` | |
| 244 | + | TargetBranch string `protobuf:"bytes,5,opt,name=target_branch,json=targetBranch,proto3" json:"target_branch,omitempty"` | |
| 245 | + | State MergeRequestState `protobuf:"varint,6,opt,name=state,proto3,enum=ermite.storage.MergeRequestState" json:"state,omitempty"` | |
| 246 | + | // Label of the token that opened it. | |
| 247 | + | Author string `protobuf:"bytes,7,opt,name=author,proto3" json:"author,omitempty"` | |
| 248 | + | CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` | |
| 249 | + | UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` | |
| 250 | + | // Head of the source branch when the request was merged or closed; empty | |
| 251 | + | // while it is open (the head is read from Git then). | |
| 252 | + | HeadId string `protobuf:"bytes,10,opt,name=head_id,json=headId,proto3" json:"head_id,omitempty"` | |
| 253 | + | // Merge base of the target and the head when the request was merged or | |
| 254 | + | // closed, so that its diff stays readable after the merge. | |
| 255 | + | BaseId string `protobuf:"bytes,11,opt,name=base_id,json=baseId,proto3" json:"base_id,omitempty"` | |
| 256 | + | MergedCommitId string `protobuf:"bytes,12,opt,name=merged_commit_id,json=mergedCommitId,proto3" json:"merged_commit_id,omitempty"` | |
| 257 | + | MergeMode MergeMode `protobuf:"varint,13,opt,name=merge_mode,json=mergeMode,proto3,enum=ermite.storage.MergeMode" json:"merge_mode,omitempty"` | |
| 258 | + | // Last comment id handed out for this request. | |
| 259 | + | CommentSeq int64 `protobuf:"varint,14,opt,name=comment_seq,json=commentSeq,proto3" json:"comment_seq,omitempty"` | |
| 260 | + | unknownFields protoimpl.UnknownFields | |
| 261 | + | sizeCache protoimpl.SizeCache | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | func (x *MergeRequest) Reset() { | |
| 265 | + | *x = MergeRequest{} | |
| 266 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[1] | |
| 267 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 268 | + | ms.StoreMessageInfo(mi) | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | func (x *MergeRequest) String() string { | |
| 272 | + | return protoimpl.X.MessageStringOf(x) | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | func (*MergeRequest) ProtoMessage() {} | |
| 276 | + | ||
| 277 | + | func (x *MergeRequest) ProtoReflect() protoreflect.Message { | |
| 278 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[1] | |
| 279 | + | if x != nil { | |
| 280 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 281 | + | if ms.LoadMessageInfo() == nil { | |
| 282 | + | ms.StoreMessageInfo(mi) | |
| 283 | + | } | |
| 284 | + | return ms | |
| 285 | + | } | |
| 286 | + | return mi.MessageOf(x) | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | // Deprecated: Use MergeRequest.ProtoReflect.Descriptor instead. | |
| 290 | + | func (*MergeRequest) Descriptor() ([]byte, []int) { | |
| 291 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{1} | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | func (x *MergeRequest) GetNumber() int64 { | |
| 295 | + | if x != nil { | |
| 296 | + | return x.Number | |
| 297 | + | } | |
| 298 | + | return 0 | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | func (x *MergeRequest) GetTitle() string { | |
| 302 | + | if x != nil { | |
| 303 | + | return x.Title | |
| 304 | + | } | |
| 305 | + | return "" | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | func (x *MergeRequest) GetDescription() string { | |
| 309 | + | if x != nil { | |
| 310 | + | return x.Description | |
| 311 | + | } | |
| 312 | + | return "" | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | func (x *MergeRequest) GetSourceBranch() string { | |
| 316 | + | if x != nil { | |
| 317 | + | return x.SourceBranch | |
| 318 | + | } | |
| 319 | + | return "" | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | func (x *MergeRequest) GetTargetBranch() string { | |
| 323 | + | if x != nil { | |
| 324 | + | return x.TargetBranch | |
| 325 | + | } | |
| 326 | + | return "" | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | func (x *MergeRequest) GetState() MergeRequestState { | |
| 330 | + | if x != nil { | |
| 331 | + | return x.State | |
| 332 | + | } | |
| 333 | + | return MergeRequestState_MERGE_REQUEST_STATE_UNSPECIFIED | |
| 334 | + | } | |
| 335 | + | ||
| 336 | + | func (x *MergeRequest) GetAuthor() string { | |
| 337 | + | if x != nil { | |
| 338 | + | return x.Author | |
| 339 | + | } | |
| 340 | + | return "" | |
| 341 | + | } | |
| 342 | + | ||
| 343 | + | func (x *MergeRequest) GetCreatedAt() *timestamppb.Timestamp { | |
| 344 | + | if x != nil { | |
| 345 | + | return x.CreatedAt | |
| 346 | + | } | |
| 347 | + | return nil | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | func (x *MergeRequest) GetUpdatedAt() *timestamppb.Timestamp { | |
| 351 | + | if x != nil { | |
| 352 | + | return x.UpdatedAt | |
| 353 | + | } | |
| 354 | + | return nil | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | func (x *MergeRequest) GetHeadId() string { | |
| 358 | + | if x != nil { | |
| 359 | + | return x.HeadId | |
| 360 | + | } | |
| 361 | + | return "" | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | func (x *MergeRequest) GetBaseId() string { | |
| 365 | + | if x != nil { | |
| 366 | + | return x.BaseId | |
| 367 | + | } | |
| 368 | + | return "" | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | func (x *MergeRequest) GetMergedCommitId() string { | |
| 372 | + | if x != nil { | |
| 373 | + | return x.MergedCommitId | |
| 374 | + | } | |
| 375 | + | return "" | |
| 376 | + | } | |
| 377 | + | ||
| 378 | + | func (x *MergeRequest) GetMergeMode() MergeMode { | |
| 379 | + | if x != nil { | |
| 380 | + | return x.MergeMode | |
| 381 | + | } | |
| 382 | + | return MergeMode_MERGE_MODE_UNSPECIFIED | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | func (x *MergeRequest) GetCommentSeq() int64 { | |
| 386 | + | if x != nil { | |
| 387 | + | return x.CommentSeq | |
| 388 | + | } | |
| 389 | + | return 0 | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | // Comment is stored under the key "mrc/<repo>/<number:010d>/<id:010d>". | |
| 393 | + | type Comment struct { | |
| 394 | + | state protoimpl.MessageState `protogen:"open.v1"` | |
| 395 | + | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` | |
| 396 | + | Author string `protobuf:"bytes,2,opt,name=author,proto3" json:"author,omitempty"` | |
| 397 | + | Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` | |
| 398 | + | Line int32 `protobuf:"varint,4,opt,name=line,proto3" json:"line,omitempty"` | |
| 399 | + | Side DiffSide `protobuf:"varint,5,opt,name=side,proto3,enum=ermite.storage.DiffSide" json:"side,omitempty"` | |
| 400 | + | CommitId string `protobuf:"bytes,6,opt,name=commit_id,json=commitId,proto3" json:"commit_id,omitempty"` | |
| 401 | + | Text string `protobuf:"bytes,7,opt,name=text,proto3" json:"text,omitempty"` | |
| 402 | + | Resolved bool `protobuf:"varint,8,opt,name=resolved,proto3" json:"resolved,omitempty"` | |
| 403 | + | CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` | |
| 404 | + | unknownFields protoimpl.UnknownFields | |
| 405 | + | sizeCache protoimpl.SizeCache | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | func (x *Comment) Reset() { | |
| 409 | + | *x = Comment{} | |
| 410 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[2] | |
| 411 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 412 | + | ms.StoreMessageInfo(mi) | |
| 413 | + | } | |
| 414 | + | ||
| 415 | + | func (x *Comment) String() string { | |
| 416 | + | return protoimpl.X.MessageStringOf(x) | |
| 417 | + | } | |
| 418 | + | ||
| 419 | + | func (*Comment) ProtoMessage() {} | |
| 420 | + | ||
| 421 | + | func (x *Comment) ProtoReflect() protoreflect.Message { | |
| 422 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[2] | |
| 423 | + | if x != nil { | |
| 424 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 425 | + | if ms.LoadMessageInfo() == nil { | |
| 426 | + | ms.StoreMessageInfo(mi) | |
| 427 | + | } | |
| 428 | + | return ms | |
| 429 | + | } | |
| 430 | + | return mi.MessageOf(x) | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | // Deprecated: Use Comment.ProtoReflect.Descriptor instead. | |
| 434 | + | func (*Comment) Descriptor() ([]byte, []int) { | |
| 435 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{2} | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | func (x *Comment) GetId() int64 { | |
| 439 | + | if x != nil { | |
| 440 | + | return x.Id | |
| 441 | + | } | |
| 442 | + | return 0 | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | func (x *Comment) GetAuthor() string { | |
| 446 | + | if x != nil { | |
| 447 | + | return x.Author | |
| 448 | + | } | |
| 449 | + | return "" | |
| 450 | + | } | |
| 451 | + | ||
| 452 | + | func (x *Comment) GetPath() string { | |
| 453 | + | if x != nil { | |
| 454 | + | return x.Path | |
| 455 | + | } | |
| 456 | + | return "" | |
| 457 | + | } | |
| 458 | + | ||
| 459 | + | func (x *Comment) GetLine() int32 { | |
| 460 | + | if x != nil { | |
| 461 | + | return x.Line | |
| 462 | + | } | |
| 463 | + | return 0 | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | func (x *Comment) GetSide() DiffSide { | |
| 467 | + | if x != nil { | |
| 468 | + | return x.Side | |
| 469 | + | } | |
| 470 | + | return DiffSide_DIFF_SIDE_UNSPECIFIED | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | func (x *Comment) GetCommitId() string { | |
| 474 | + | if x != nil { | |
| 475 | + | return x.CommitId | |
| 476 | + | } | |
| 477 | + | return "" | |
| 478 | + | } | |
| 479 | + | ||
| 480 | + | func (x *Comment) GetText() string { | |
| 481 | + | if x != nil { | |
| 482 | + | return x.Text | |
| 483 | + | } | |
| 484 | + | return "" | |
| 485 | + | } | |
| 486 | + | ||
| 487 | + | func (x *Comment) GetResolved() bool { | |
| 488 | + | if x != nil { | |
| 489 | + | return x.Resolved | |
| 490 | + | } | |
| 491 | + | return false | |
| 492 | + | } | |
| 493 | + | ||
| 494 | + | func (x *Comment) GetCreatedAt() *timestamppb.Timestamp { | |
| 495 | + | if x != nil { | |
| 496 | + | return x.CreatedAt | |
| 497 | + | } | |
| 498 | + | return nil | |
| 499 | + | } | |
| 500 | + | ||
| 501 | + | // Sequence is stored under the key "mrseq/<repo>": the last merge request | |
| 502 | + | // number handed out for the repository. | |
| 503 | + | type Sequence struct { | |
| 504 | + | state protoimpl.MessageState `protogen:"open.v1"` | |
| 505 | + | Last int64 `protobuf:"varint,1,opt,name=last,proto3" json:"last,omitempty"` | |
| 506 | + | unknownFields protoimpl.UnknownFields | |
| 507 | + | sizeCache protoimpl.SizeCache | |
| 508 | + | } | |
| 509 | + | ||
| 510 | + | func (x *Sequence) Reset() { | |
| 511 | + | *x = Sequence{} | |
| 512 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[3] | |
| 513 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 514 | + | ms.StoreMessageInfo(mi) | |
| 515 | + | } | |
| 516 | + | ||
| 517 | + | func (x *Sequence) String() string { | |
| 518 | + | return protoimpl.X.MessageStringOf(x) | |
| 519 | + | } | |
| 520 | + | ||
| 521 | + | func (*Sequence) ProtoMessage() {} | |
| 522 | + | ||
| 523 | + | func (x *Sequence) ProtoReflect() protoreflect.Message { | |
| 524 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[3] | |
| 525 | + | if x != nil { | |
| 526 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 527 | + | if ms.LoadMessageInfo() == nil { | |
| 528 | + | ms.StoreMessageInfo(mi) | |
| 529 | + | } | |
| 530 | + | return ms | |
| 531 | + | } | |
| 532 | + | return mi.MessageOf(x) | |
| 533 | + | } | |
| 534 | + | ||
| 535 | + | // Deprecated: Use Sequence.ProtoReflect.Descriptor instead. | |
| 536 | + | func (*Sequence) Descriptor() ([]byte, []int) { | |
| 537 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{3} | |
| 538 | + | } | |
| 539 | + | ||
| 540 | + | func (x *Sequence) GetLast() int64 { | |
| 541 | + | if x != nil { | |
| 542 | + | return x.Last | |
| 543 | + | } | |
| 544 | + | return 0 | |
| 545 | + | } | |
| 546 | + | ||
| 547 | + | var File_ermite_storage_merge_request_proto protoreflect.FileDescriptor | |
| 548 | + | ||
| 549 | + | const file_ermite_storage_merge_request_proto_rawDesc = "" + | |
| 550 | + | "\n" + | |
| 551 | + | "\"ermite/storage/merge_request.proto\x12\x0eermite.storage\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8b\x01\n" + | |
| 552 | + | "\fRepoSettings\x12I\n" + | |
| 553 | + | "\x13allowed_merge_modes\x18\x01 \x03(\x0e2\x19.ermite.storage.MergeModeR\x11allowedMergeModes\x120\n" + | |
| 554 | + | "\x14failing_checks_block\x18\x02 \x01(\bR\x12failingChecksBlock\"\xa6\x04\n" + | |
| 555 | + | "\fMergeRequest\x12\x16\n" + | |
| 556 | + | "\x06number\x18\x01 \x01(\x03R\x06number\x12\x14\n" + | |
| 557 | + | "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + | |
| 558 | + | "\vdescription\x18\x03 \x01(\tR\vdescription\x12#\n" + | |
| 559 | + | "\rsource_branch\x18\x04 \x01(\tR\fsourceBranch\x12#\n" + | |
| 560 | + | "\rtarget_branch\x18\x05 \x01(\tR\ftargetBranch\x127\n" + | |
| 561 | + | "\x05state\x18\x06 \x01(\x0e2!.ermite.storage.MergeRequestStateR\x05state\x12\x16\n" + | |
| 562 | + | "\x06author\x18\a \x01(\tR\x06author\x129\n" + | |
| 563 | + | "\n" + | |
| 564 | + | "created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + | |
| 565 | + | "\n" + | |
| 566 | + | "updated_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x17\n" + | |
| 567 | + | "\ahead_id\x18\n" + | |
| 568 | + | " \x01(\tR\x06headId\x12\x17\n" + | |
| 569 | + | "\abase_id\x18\v \x01(\tR\x06baseId\x12(\n" + | |
| 570 | + | "\x10merged_commit_id\x18\f \x01(\tR\x0emergedCommitId\x128\n" + | |
| 571 | + | "\n" + | |
| 572 | + | "merge_mode\x18\r \x01(\x0e2\x19.ermite.storage.MergeModeR\tmergeMode\x12\x1f\n" + | |
| 573 | + | "\vcomment_seq\x18\x0e \x01(\x03R\n" + | |
| 574 | + | "commentSeq\"\x8f\x02\n" + | |
| 575 | + | "\aComment\x12\x0e\n" + | |
| 576 | + | "\x02id\x18\x01 \x01(\x03R\x02id\x12\x16\n" + | |
| 577 | + | "\x06author\x18\x02 \x01(\tR\x06author\x12\x12\n" + | |
| 578 | + | "\x04path\x18\x03 \x01(\tR\x04path\x12\x12\n" + | |
| 579 | + | "\x04line\x18\x04 \x01(\x05R\x04line\x12,\n" + | |
| 580 | + | "\x04side\x18\x05 \x01(\x0e2\x18.ermite.storage.DiffSideR\x04side\x12\x1b\n" + | |
| 581 | + | "\tcommit_id\x18\x06 \x01(\tR\bcommitId\x12\x12\n" + | |
| 582 | + | "\x04text\x18\a \x01(\tR\x04text\x12\x1a\n" + | |
| 583 | + | "\bresolved\x18\b \x01(\bR\bresolved\x129\n" + | |
| 584 | + | "\n" + | |
| 585 | + | "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\x1e\n" + | |
| 586 | + | "\bSequence\x12\x12\n" + | |
| 587 | + | "\x04last\x18\x01 \x01(\x03R\x04last*[\n" + | |
| 588 | + | "\tMergeMode\x12\x1a\n" + | |
| 589 | + | "\x16MERGE_MODE_UNSPECIFIED\x10\x00\x12\x1b\n" + | |
| 590 | + | "\x17MERGE_MODE_FAST_FORWARD\x10\x01\x12\x15\n" + | |
| 591 | + | "\x11MERGE_MODE_SQUASH\x10\x02*\x96\x01\n" + | |
| 592 | + | "\x11MergeRequestState\x12#\n" + | |
| 593 | + | "\x1fMERGE_REQUEST_STATE_UNSPECIFIED\x10\x00\x12\x1c\n" + | |
| 594 | + | "\x18MERGE_REQUEST_STATE_OPEN\x10\x01\x12\x1e\n" + | |
| 595 | + | "\x1aMERGE_REQUEST_STATE_MERGED\x10\x02\x12\x1e\n" + | |
| 596 | + | "\x1aMERGE_REQUEST_STATE_CLOSED\x10\x03*K\n" + | |
| 597 | + | "\bDiffSide\x12\x19\n" + | |
| 598 | + | "\x15DIFF_SIDE_UNSPECIFIED\x10\x00\x12\x11\n" + | |
| 599 | + | "\rDIFF_SIDE_OLD\x10\x01\x12\x11\n" + | |
| 600 | + | "\rDIFF_SIDE_NEW\x10\x02B/Z-bnema.dev/ermite/gen/ermite/storage;storagepbb\x06proto3" | |
| 601 | + | ||
| 602 | + | var ( | |
| 603 | + | file_ermite_storage_merge_request_proto_rawDescOnce sync.Once | |
| 604 | + | file_ermite_storage_merge_request_proto_rawDescData []byte | |
| 605 | + | ) | |
| 606 | + | ||
| 607 | + | func file_ermite_storage_merge_request_proto_rawDescGZIP() []byte { | |
| 608 | + | file_ermite_storage_merge_request_proto_rawDescOnce.Do(func() { | |
| 609 | + | file_ermite_storage_merge_request_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ermite_storage_merge_request_proto_rawDesc), len(file_ermite_storage_merge_request_proto_rawDesc))) | |
| 610 | + | }) | |
| 611 | + | return file_ermite_storage_merge_request_proto_rawDescData | |
| 612 | + | } | |
| 613 | + | ||
| 614 | + | var file_ermite_storage_merge_request_proto_enumTypes = make([]protoimpl.EnumInfo, 3) | |
| 615 | + | var file_ermite_storage_merge_request_proto_msgTypes = make([]protoimpl.MessageInfo, 4) | |
| 616 | + | var file_ermite_storage_merge_request_proto_goTypes = []any{ | |
| 617 | + | (MergeMode)(0), // 0: ermite.storage.MergeMode | |
| 618 | + | (MergeRequestState)(0), // 1: ermite.storage.MergeRequestState | |
| 619 | + | (DiffSide)(0), // 2: ermite.storage.DiffSide | |
| 620 | + | (*RepoSettings)(nil), // 3: ermite.storage.RepoSettings | |
| 621 | + | (*MergeRequest)(nil), // 4: ermite.storage.MergeRequest | |
| 622 | + | (*Comment)(nil), // 5: ermite.storage.Comment | |
| 623 | + | (*Sequence)(nil), // 6: ermite.storage.Sequence | |
| 624 | + | (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp | |
| 625 | + | } | |
| 626 | + | var file_ermite_storage_merge_request_proto_depIdxs = []int32{ | |
| 627 | + | 0, // 0: ermite.storage.RepoSettings.allowed_merge_modes:type_name -> ermite.storage.MergeMode | |
| 628 | + | 1, // 1: ermite.storage.MergeRequest.state:type_name -> ermite.storage.MergeRequestState | |
| 629 | + | 7, // 2: ermite.storage.MergeRequest.created_at:type_name -> google.protobuf.Timestamp | |
| 630 | + | 7, // 3: ermite.storage.MergeRequest.updated_at:type_name -> google.protobuf.Timestamp | |
| 631 | + | 0, // 4: ermite.storage.MergeRequest.merge_mode:type_name -> ermite.storage.MergeMode | |
| 632 | + | 2, // 5: ermite.storage.Comment.side:type_name -> ermite.storage.DiffSide | |
| 633 | + | 7, // 6: ermite.storage.Comment.created_at:type_name -> google.protobuf.Timestamp | |
| 634 | + | 7, // [7:7] is the sub-list for method output_type | |
| 635 | + | 7, // [7:7] is the sub-list for method input_type | |
| 636 | + | 7, // [7:7] is the sub-list for extension type_name | |
| 637 | + | 7, // [7:7] is the sub-list for extension extendee | |
| 638 | + | 0, // [0:7] is the sub-list for field type_name | |
| 639 | + | } | |
| 640 | + | ||
| 641 | + | func init() { file_ermite_storage_merge_request_proto_init() } | |
| 642 | + | func file_ermite_storage_merge_request_proto_init() { | |
| 643 | + | if File_ermite_storage_merge_request_proto != nil { | |
| 644 | + | return | |
| 645 | + | } | |
| 646 | + | type x struct{} | |
| 647 | + | out := protoimpl.TypeBuilder{ | |
| 648 | + | File: protoimpl.DescBuilder{ | |
| 649 | + | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | |
| 650 | + | RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_storage_merge_request_proto_rawDesc), len(file_ermite_storage_merge_request_proto_rawDesc)), | |
| 651 | + | NumEnums: 3, | |
| 652 | + | NumMessages: 4, | |
| 653 | + | NumExtensions: 0, | |
| 654 | + | NumServices: 0, | |
| 655 | + | }, | |
| 656 | + | GoTypes: file_ermite_storage_merge_request_proto_goTypes, | |
| 657 | + | DependencyIndexes: file_ermite_storage_merge_request_proto_depIdxs, | |
| 658 | + | EnumInfos: file_ermite_storage_merge_request_proto_enumTypes, | |
| 659 | + | MessageInfos: file_ermite_storage_merge_request_proto_msgTypes, | |
| 660 | + | }.Build() | |
| 661 | + | File_ermite_storage_merge_request_proto = out.File | |
| 662 | + | file_ermite_storage_merge_request_proto_goTypes = nil | |
| 663 | + | file_ermite_storage_merge_request_proto_depIdxs = nil | |
| 664 | + | } | |
| @@ -0,0 +1,664 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | // Internal, unversioned storage schema of ermite: merge requests. | |||
| 4 | + | ||||
| 5 | + | // Code generated by protoc-gen-go. DO NOT EDIT. | |||
| 6 | + | // versions: | |||
| 7 | + | // protoc-gen-go v1.36.12 | |||
| 8 | + | // protoc (unknown) | |||
| 9 | + | // source: ermite/storage/merge_request.proto | |||
| 10 | + | ||||
| 11 | + | package storagepb | |||
| 12 | + | ||||
| 13 | + | import ( | |||
| 14 | + | protoreflect "google.golang.org/protobuf/reflect/protoreflect" | |||
| 15 | + | protoimpl "google.golang.org/protobuf/runtime/protoimpl" | |||
| 16 | + | timestamppb "google.golang.org/protobuf/types/known/timestamppb" | |||
| 17 | + | reflect "reflect" | |||
| 18 | + | sync "sync" | |||
| 19 | + | unsafe "unsafe" | |||
| 20 | + | ) | |||
| 21 | + | ||||
| 22 | + | const ( | |||
| 23 | + | // Verify that this generated code is sufficiently up-to-date. | |||
| 24 | + | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) | |||
| 25 | + | // Verify that runtime/protoimpl is sufficiently up-to-date. | |||
| 26 | + | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | // MergeMode is how a merge request is merged. | |||
| 30 | + | type MergeMode int32 | |||
| 31 | + | ||||
| 32 | + | const ( | |||
| 33 | + | MergeMode_MERGE_MODE_UNSPECIFIED MergeMode = 0 | |||
| 34 | + | MergeMode_MERGE_MODE_FAST_FORWARD MergeMode = 1 | |||
| 35 | + | MergeMode_MERGE_MODE_SQUASH MergeMode = 2 | |||
| 36 | + | ) | |||
| 37 | + | ||||
| 38 | + | // Enum value maps for MergeMode. | |||
| 39 | + | var ( | |||
| 40 | + | MergeMode_name = map[int32]string{ | |||
| 41 | + | 0: "MERGE_MODE_UNSPECIFIED", | |||
| 42 | + | 1: "MERGE_MODE_FAST_FORWARD", | |||
| 43 | + | 2: "MERGE_MODE_SQUASH", | |||
| 44 | + | } | |||
| 45 | + | MergeMode_value = map[string]int32{ | |||
| 46 | + | "MERGE_MODE_UNSPECIFIED": 0, | |||
| 47 | + | "MERGE_MODE_FAST_FORWARD": 1, | |||
| 48 | + | "MERGE_MODE_SQUASH": 2, | |||
| 49 | + | } | |||
| 50 | + | ) | |||
| 51 | + | ||||
| 52 | + | func (x MergeMode) Enum() *MergeMode { | |||
| 53 | + | p := new(MergeMode) | |||
| 54 | + | *p = x | |||
| 55 | + | return p | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | func (x MergeMode) String() string { | |||
| 59 | + | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func (MergeMode) Descriptor() protoreflect.EnumDescriptor { | |||
| 63 | + | return file_ermite_storage_merge_request_proto_enumTypes[0].Descriptor() | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (MergeMode) Type() protoreflect.EnumType { | |||
| 67 | + | return &file_ermite_storage_merge_request_proto_enumTypes[0] | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | func (x MergeMode) Number() protoreflect.EnumNumber { | |||
| 71 | + | return protoreflect.EnumNumber(x) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // Deprecated: Use MergeMode.Descriptor instead. | |||
| 75 | + | func (MergeMode) EnumDescriptor() ([]byte, []int) { | |||
| 76 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{0} | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // MergeRequestState is where a merge request is in its life. | |||
| 80 | + | type MergeRequestState int32 | |||
| 81 | + | ||||
| 82 | + | const ( | |||
| 83 | + | MergeRequestState_MERGE_REQUEST_STATE_UNSPECIFIED MergeRequestState = 0 | |||
| 84 | + | MergeRequestState_MERGE_REQUEST_STATE_OPEN MergeRequestState = 1 | |||
| 85 | + | MergeRequestState_MERGE_REQUEST_STATE_MERGED MergeRequestState = 2 | |||
| 86 | + | MergeRequestState_MERGE_REQUEST_STATE_CLOSED MergeRequestState = 3 | |||
| 87 | + | ) | |||
| 88 | + | ||||
| 89 | + | // Enum value maps for MergeRequestState. | |||
| 90 | + | var ( | |||
| 91 | + | MergeRequestState_name = map[int32]string{ | |||
| 92 | + | 0: "MERGE_REQUEST_STATE_UNSPECIFIED", | |||
| 93 | + | 1: "MERGE_REQUEST_STATE_OPEN", | |||
| 94 | + | 2: "MERGE_REQUEST_STATE_MERGED", | |||
| 95 | + | 3: "MERGE_REQUEST_STATE_CLOSED", | |||
| 96 | + | } | |||
| 97 | + | MergeRequestState_value = map[string]int32{ | |||
| 98 | + | "MERGE_REQUEST_STATE_UNSPECIFIED": 0, | |||
| 99 | + | "MERGE_REQUEST_STATE_OPEN": 1, | |||
| 100 | + | "MERGE_REQUEST_STATE_MERGED": 2, | |||
| 101 | + | "MERGE_REQUEST_STATE_CLOSED": 3, | |||
| 102 | + | } | |||
| 103 | + | ) | |||
| 104 | + | ||||
| 105 | + | func (x MergeRequestState) Enum() *MergeRequestState { | |||
| 106 | + | p := new(MergeRequestState) | |||
| 107 | + | *p = x | |||
| 108 | + | return p | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func (x MergeRequestState) String() string { | |||
| 112 | + | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func (MergeRequestState) Descriptor() protoreflect.EnumDescriptor { | |||
| 116 | + | return file_ermite_storage_merge_request_proto_enumTypes[1].Descriptor() | |||
| 117 | + | } | |||
| 118 | + | ||||
| 119 | + | func (MergeRequestState) Type() protoreflect.EnumType { | |||
| 120 | + | return &file_ermite_storage_merge_request_proto_enumTypes[1] | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func (x MergeRequestState) Number() protoreflect.EnumNumber { | |||
| 124 | + | return protoreflect.EnumNumber(x) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | // Deprecated: Use MergeRequestState.Descriptor instead. | |||
| 128 | + | func (MergeRequestState) EnumDescriptor() ([]byte, []int) { | |||
| 129 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{1} | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // DiffSide is the side of a diff a comment is attached to. | |||
| 133 | + | type DiffSide int32 | |||
| 134 | + | ||||
| 135 | + | const ( | |||
| 136 | + | // A general comment. | |||
| 137 | + | DiffSide_DIFF_SIDE_UNSPECIFIED DiffSide = 0 | |||
| 138 | + | DiffSide_DIFF_SIDE_OLD DiffSide = 1 | |||
| 139 | + | DiffSide_DIFF_SIDE_NEW DiffSide = 2 | |||
| 140 | + | ) | |||
| 141 | + | ||||
| 142 | + | // Enum value maps for DiffSide. | |||
| 143 | + | var ( | |||
| 144 | + | DiffSide_name = map[int32]string{ | |||
| 145 | + | 0: "DIFF_SIDE_UNSPECIFIED", | |||
| 146 | + | 1: "DIFF_SIDE_OLD", | |||
| 147 | + | 2: "DIFF_SIDE_NEW", | |||
| 148 | + | } | |||
| 149 | + | DiffSide_value = map[string]int32{ | |||
| 150 | + | "DIFF_SIDE_UNSPECIFIED": 0, | |||
| 151 | + | "DIFF_SIDE_OLD": 1, | |||
| 152 | + | "DIFF_SIDE_NEW": 2, | |||
| 153 | + | } | |||
| 154 | + | ) | |||
| 155 | + | ||||
| 156 | + | func (x DiffSide) Enum() *DiffSide { | |||
| 157 | + | p := new(DiffSide) | |||
| 158 | + | *p = x | |||
| 159 | + | return p | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | func (x DiffSide) String() string { | |||
| 163 | + | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | func (DiffSide) Descriptor() protoreflect.EnumDescriptor { | |||
| 167 | + | return file_ermite_storage_merge_request_proto_enumTypes[2].Descriptor() | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | func (DiffSide) Type() protoreflect.EnumType { | |||
| 171 | + | return &file_ermite_storage_merge_request_proto_enumTypes[2] | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | func (x DiffSide) Number() protoreflect.EnumNumber { | |||
| 175 | + | return protoreflect.EnumNumber(x) | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | // Deprecated: Use DiffSide.Descriptor instead. | |||
| 179 | + | func (DiffSide) EnumDescriptor() ([]byte, []int) { | |||
| 180 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{2} | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | // RepoSettings are the merge settings of a repository, stored inside Repo. | |||
| 184 | + | type RepoSettings struct { | |||
| 185 | + | state protoimpl.MessageState `protogen:"open.v1"` | |||
| 186 | + | // Never empty once stored. | |||
| 187 | + | AllowedMergeModes []MergeMode `protobuf:"varint,1,rep,packed,name=allowed_merge_modes,json=allowedMergeModes,proto3,enum=ermite.storage.MergeMode" json:"allowed_merge_modes,omitempty"` | |||
| 188 | + | FailingChecksBlock bool `protobuf:"varint,2,opt,name=failing_checks_block,json=failingChecksBlock,proto3" json:"failing_checks_block,omitempty"` | |||
| 189 | + | unknownFields protoimpl.UnknownFields | |||
| 190 | + | sizeCache protoimpl.SizeCache | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | func (x *RepoSettings) Reset() { | |||
| 194 | + | *x = RepoSettings{} | |||
| 195 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[0] | |||
| 196 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 197 | + | ms.StoreMessageInfo(mi) | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | func (x *RepoSettings) String() string { | |||
| 201 | + | return protoimpl.X.MessageStringOf(x) | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | func (*RepoSettings) ProtoMessage() {} | |||
| 205 | + | ||||
| 206 | + | func (x *RepoSettings) ProtoReflect() protoreflect.Message { | |||
| 207 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[0] | |||
| 208 | + | if x != nil { | |||
| 209 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 210 | + | if ms.LoadMessageInfo() == nil { | |||
| 211 | + | ms.StoreMessageInfo(mi) | |||
| 212 | + | } | |||
| 213 | + | return ms | |||
| 214 | + | } | |||
| 215 | + | return mi.MessageOf(x) | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | // Deprecated: Use RepoSettings.ProtoReflect.Descriptor instead. | |||
| 219 | + | func (*RepoSettings) Descriptor() ([]byte, []int) { | |||
| 220 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{0} | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | func (x *RepoSettings) GetAllowedMergeModes() []MergeMode { | |||
| 224 | + | if x != nil { | |||
| 225 | + | return x.AllowedMergeModes | |||
| 226 | + | } | |||
| 227 | + | return nil | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | func (x *RepoSettings) GetFailingChecksBlock() bool { | |||
| 231 | + | if x != nil { | |||
| 232 | + | return x.FailingChecksBlock | |||
| 233 | + | } | |||
| 234 | + | return false | |||
| 235 | + | } | |||
| 236 | + | ||||
| 237 | + | // MergeRequest is stored under the key "mr/<repo>/<number:010d>". | |||
| 238 | + | type MergeRequest struct { | |||
| 239 | + | state protoimpl.MessageState `protogen:"open.v1"` | |||
| 240 | + | Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` | |||
| 241 | + | Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` | |||
| 242 | + | Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` | |||
| 243 | + | SourceBranch string `protobuf:"bytes,4,opt,name=source_branch,json=sourceBranch,proto3" json:"source_branch,omitempty"` | |||
| 244 | + | TargetBranch string `protobuf:"bytes,5,opt,name=target_branch,json=targetBranch,proto3" json:"target_branch,omitempty"` | |||
| 245 | + | State MergeRequestState `protobuf:"varint,6,opt,name=state,proto3,enum=ermite.storage.MergeRequestState" json:"state,omitempty"` | |||
| 246 | + | // Label of the token that opened it. | |||
| 247 | + | Author string `protobuf:"bytes,7,opt,name=author,proto3" json:"author,omitempty"` | |||
| 248 | + | CreatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` | |||
| 249 | + | UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` | |||
| 250 | + | // Head of the source branch when the request was merged or closed; empty | |||
| 251 | + | // while it is open (the head is read from Git then). | |||
| 252 | + | HeadId string `protobuf:"bytes,10,opt,name=head_id,json=headId,proto3" json:"head_id,omitempty"` | |||
| 253 | + | // Merge base of the target and the head when the request was merged or | |||
| 254 | + | // closed, so that its diff stays readable after the merge. | |||
| 255 | + | BaseId string `protobuf:"bytes,11,opt,name=base_id,json=baseId,proto3" json:"base_id,omitempty"` | |||
| 256 | + | MergedCommitId string `protobuf:"bytes,12,opt,name=merged_commit_id,json=mergedCommitId,proto3" json:"merged_commit_id,omitempty"` | |||
| 257 | + | MergeMode MergeMode `protobuf:"varint,13,opt,name=merge_mode,json=mergeMode,proto3,enum=ermite.storage.MergeMode" json:"merge_mode,omitempty"` | |||
| 258 | + | // Last comment id handed out for this request. | |||
| 259 | + | CommentSeq int64 `protobuf:"varint,14,opt,name=comment_seq,json=commentSeq,proto3" json:"comment_seq,omitempty"` | |||
| 260 | + | unknownFields protoimpl.UnknownFields | |||
| 261 | + | sizeCache protoimpl.SizeCache | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | func (x *MergeRequest) Reset() { | |||
| 265 | + | *x = MergeRequest{} | |||
| 266 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[1] | |||
| 267 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 268 | + | ms.StoreMessageInfo(mi) | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | func (x *MergeRequest) String() string { | |||
| 272 | + | return protoimpl.X.MessageStringOf(x) | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | func (*MergeRequest) ProtoMessage() {} | |||
| 276 | + | ||||
| 277 | + | func (x *MergeRequest) ProtoReflect() protoreflect.Message { | |||
| 278 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[1] | |||
| 279 | + | if x != nil { | |||
| 280 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 281 | + | if ms.LoadMessageInfo() == nil { | |||
| 282 | + | ms.StoreMessageInfo(mi) | |||
| 283 | + | } | |||
| 284 | + | return ms | |||
| 285 | + | } | |||
| 286 | + | return mi.MessageOf(x) | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | // Deprecated: Use MergeRequest.ProtoReflect.Descriptor instead. | |||
| 290 | + | func (*MergeRequest) Descriptor() ([]byte, []int) { | |||
| 291 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{1} | |||
| 292 | + | } | |||
| 293 | + | ||||
| 294 | + | func (x *MergeRequest) GetNumber() int64 { | |||
| 295 | + | if x != nil { | |||
| 296 | + | return x.Number | |||
| 297 | + | } | |||
| 298 | + | return 0 | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | func (x *MergeRequest) GetTitle() string { | |||
| 302 | + | if x != nil { | |||
| 303 | + | return x.Title | |||
| 304 | + | } | |||
| 305 | + | return "" | |||
| 306 | + | } | |||
| 307 | + | ||||
| 308 | + | func (x *MergeRequest) GetDescription() string { | |||
| 309 | + | if x != nil { | |||
| 310 | + | return x.Description | |||
| 311 | + | } | |||
| 312 | + | return "" | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | func (x *MergeRequest) GetSourceBranch() string { | |||
| 316 | + | if x != nil { | |||
| 317 | + | return x.SourceBranch | |||
| 318 | + | } | |||
| 319 | + | return "" | |||
| 320 | + | } | |||
| 321 | + | ||||
| 322 | + | func (x *MergeRequest) GetTargetBranch() string { | |||
| 323 | + | if x != nil { | |||
| 324 | + | return x.TargetBranch | |||
| 325 | + | } | |||
| 326 | + | return "" | |||
| 327 | + | } | |||
| 328 | + | ||||
| 329 | + | func (x *MergeRequest) GetState() MergeRequestState { | |||
| 330 | + | if x != nil { | |||
| 331 | + | return x.State | |||
| 332 | + | } | |||
| 333 | + | return MergeRequestState_MERGE_REQUEST_STATE_UNSPECIFIED | |||
| 334 | + | } | |||
| 335 | + | ||||
| 336 | + | func (x *MergeRequest) GetAuthor() string { | |||
| 337 | + | if x != nil { | |||
| 338 | + | return x.Author | |||
| 339 | + | } | |||
| 340 | + | return "" | |||
| 341 | + | } | |||
| 342 | + | ||||
| 343 | + | func (x *MergeRequest) GetCreatedAt() *timestamppb.Timestamp { | |||
| 344 | + | if x != nil { | |||
| 345 | + | return x.CreatedAt | |||
| 346 | + | } | |||
| 347 | + | return nil | |||
| 348 | + | } | |||
| 349 | + | ||||
| 350 | + | func (x *MergeRequest) GetUpdatedAt() *timestamppb.Timestamp { | |||
| 351 | + | if x != nil { | |||
| 352 | + | return x.UpdatedAt | |||
| 353 | + | } | |||
| 354 | + | return nil | |||
| 355 | + | } | |||
| 356 | + | ||||
| 357 | + | func (x *MergeRequest) GetHeadId() string { | |||
| 358 | + | if x != nil { | |||
| 359 | + | return x.HeadId | |||
| 360 | + | } | |||
| 361 | + | return "" | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | func (x *MergeRequest) GetBaseId() string { | |||
| 365 | + | if x != nil { | |||
| 366 | + | return x.BaseId | |||
| 367 | + | } | |||
| 368 | + | return "" | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | func (x *MergeRequest) GetMergedCommitId() string { | |||
| 372 | + | if x != nil { | |||
| 373 | + | return x.MergedCommitId | |||
| 374 | + | } | |||
| 375 | + | return "" | |||
| 376 | + | } | |||
| 377 | + | ||||
| 378 | + | func (x *MergeRequest) GetMergeMode() MergeMode { | |||
| 379 | + | if x != nil { | |||
| 380 | + | return x.MergeMode | |||
| 381 | + | } | |||
| 382 | + | return MergeMode_MERGE_MODE_UNSPECIFIED | |||
| 383 | + | } | |||
| 384 | + | ||||
| 385 | + | func (x *MergeRequest) GetCommentSeq() int64 { | |||
| 386 | + | if x != nil { | |||
| 387 | + | return x.CommentSeq | |||
| 388 | + | } | |||
| 389 | + | return 0 | |||
| 390 | + | } | |||
| 391 | + | ||||
| 392 | + | // Comment is stored under the key "mrc/<repo>/<number:010d>/<id:010d>". | |||
| 393 | + | type Comment struct { | |||
| 394 | + | state protoimpl.MessageState `protogen:"open.v1"` | |||
| 395 | + | Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` | |||
| 396 | + | Author string `protobuf:"bytes,2,opt,name=author,proto3" json:"author,omitempty"` | |||
| 397 | + | Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` | |||
| 398 | + | Line int32 `protobuf:"varint,4,opt,name=line,proto3" json:"line,omitempty"` | |||
| 399 | + | Side DiffSide `protobuf:"varint,5,opt,name=side,proto3,enum=ermite.storage.DiffSide" json:"side,omitempty"` | |||
| 400 | + | CommitId string `protobuf:"bytes,6,opt,name=commit_id,json=commitId,proto3" json:"commit_id,omitempty"` | |||
| 401 | + | Text string `protobuf:"bytes,7,opt,name=text,proto3" json:"text,omitempty"` | |||
| 402 | + | Resolved bool `protobuf:"varint,8,opt,name=resolved,proto3" json:"resolved,omitempty"` | |||
| 403 | + | CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` | |||
| 404 | + | unknownFields protoimpl.UnknownFields | |||
| 405 | + | sizeCache protoimpl.SizeCache | |||
| 406 | + | } | |||
| 407 | + | ||||
| 408 | + | func (x *Comment) Reset() { | |||
| 409 | + | *x = Comment{} | |||
| 410 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[2] | |||
| 411 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 412 | + | ms.StoreMessageInfo(mi) | |||
| 413 | + | } | |||
| 414 | + | ||||
| 415 | + | func (x *Comment) String() string { | |||
| 416 | + | return protoimpl.X.MessageStringOf(x) | |||
| 417 | + | } | |||
| 418 | + | ||||
| 419 | + | func (*Comment) ProtoMessage() {} | |||
| 420 | + | ||||
| 421 | + | func (x *Comment) ProtoReflect() protoreflect.Message { | |||
| 422 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[2] | |||
| 423 | + | if x != nil { | |||
| 424 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 425 | + | if ms.LoadMessageInfo() == nil { | |||
| 426 | + | ms.StoreMessageInfo(mi) | |||
| 427 | + | } | |||
| 428 | + | return ms | |||
| 429 | + | } | |||
| 430 | + | return mi.MessageOf(x) | |||
| 431 | + | } | |||
| 432 | + | ||||
| 433 | + | // Deprecated: Use Comment.ProtoReflect.Descriptor instead. | |||
| 434 | + | func (*Comment) Descriptor() ([]byte, []int) { | |||
| 435 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{2} | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | func (x *Comment) GetId() int64 { | |||
| 439 | + | if x != nil { | |||
| 440 | + | return x.Id | |||
| 441 | + | } | |||
| 442 | + | return 0 | |||
| 443 | + | } | |||
| 444 | + | ||||
| 445 | + | func (x *Comment) GetAuthor() string { | |||
| 446 | + | if x != nil { | |||
| 447 | + | return x.Author | |||
| 448 | + | } | |||
| 449 | + | return "" | |||
| 450 | + | } | |||
| 451 | + | ||||
| 452 | + | func (x *Comment) GetPath() string { | |||
| 453 | + | if x != nil { | |||
| 454 | + | return x.Path | |||
| 455 | + | } | |||
| 456 | + | return "" | |||
| 457 | + | } | |||
| 458 | + | ||||
| 459 | + | func (x *Comment) GetLine() int32 { | |||
| 460 | + | if x != nil { | |||
| 461 | + | return x.Line | |||
| 462 | + | } | |||
| 463 | + | return 0 | |||
| 464 | + | } | |||
| 465 | + | ||||
| 466 | + | func (x *Comment) GetSide() DiffSide { | |||
| 467 | + | if x != nil { | |||
| 468 | + | return x.Side | |||
| 469 | + | } | |||
| 470 | + | return DiffSide_DIFF_SIDE_UNSPECIFIED | |||
| 471 | + | } | |||
| 472 | + | ||||
| 473 | + | func (x *Comment) GetCommitId() string { | |||
| 474 | + | if x != nil { | |||
| 475 | + | return x.CommitId | |||
| 476 | + | } | |||
| 477 | + | return "" | |||
| 478 | + | } | |||
| 479 | + | ||||
| 480 | + | func (x *Comment) GetText() string { | |||
| 481 | + | if x != nil { | |||
| 482 | + | return x.Text | |||
| 483 | + | } | |||
| 484 | + | return "" | |||
| 485 | + | } | |||
| 486 | + | ||||
| 487 | + | func (x *Comment) GetResolved() bool { | |||
| 488 | + | if x != nil { | |||
| 489 | + | return x.Resolved | |||
| 490 | + | } | |||
| 491 | + | return false | |||
| 492 | + | } | |||
| 493 | + | ||||
| 494 | + | func (x *Comment) GetCreatedAt() *timestamppb.Timestamp { | |||
| 495 | + | if x != nil { | |||
| 496 | + | return x.CreatedAt | |||
| 497 | + | } | |||
| 498 | + | return nil | |||
| 499 | + | } | |||
| 500 | + | ||||
| 501 | + | // Sequence is stored under the key "mrseq/<repo>": the last merge request | |||
| 502 | + | // number handed out for the repository. | |||
| 503 | + | type Sequence struct { | |||
| 504 | + | state protoimpl.MessageState `protogen:"open.v1"` | |||
| 505 | + | Last int64 `protobuf:"varint,1,opt,name=last,proto3" json:"last,omitempty"` | |||
| 506 | + | unknownFields protoimpl.UnknownFields | |||
| 507 | + | sizeCache protoimpl.SizeCache | |||
| 508 | + | } | |||
| 509 | + | ||||
| 510 | + | func (x *Sequence) Reset() { | |||
| 511 | + | *x = Sequence{} | |||
| 512 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[3] | |||
| 513 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 514 | + | ms.StoreMessageInfo(mi) | |||
| 515 | + | } | |||
| 516 | + | ||||
| 517 | + | func (x *Sequence) String() string { | |||
| 518 | + | return protoimpl.X.MessageStringOf(x) | |||
| 519 | + | } | |||
| 520 | + | ||||
| 521 | + | func (*Sequence) ProtoMessage() {} | |||
| 522 | + | ||||
| 523 | + | func (x *Sequence) ProtoReflect() protoreflect.Message { | |||
| 524 | + | mi := &file_ermite_storage_merge_request_proto_msgTypes[3] | |||
| 525 | + | if x != nil { | |||
| 526 | + | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 527 | + | if ms.LoadMessageInfo() == nil { | |||
| 528 | + | ms.StoreMessageInfo(mi) | |||
| 529 | + | } | |||
| 530 | + | return ms | |||
| 531 | + | } | |||
| 532 | + | return mi.MessageOf(x) | |||
| 533 | + | } | |||
| 534 | + | ||||
| 535 | + | // Deprecated: Use Sequence.ProtoReflect.Descriptor instead. | |||
| 536 | + | func (*Sequence) Descriptor() ([]byte, []int) { | |||
| 537 | + | return file_ermite_storage_merge_request_proto_rawDescGZIP(), []int{3} | |||
| 538 | + | } | |||
| 539 | + | ||||
| 540 | + | func (x *Sequence) GetLast() int64 { | |||
| 541 | + | if x != nil { | |||
| 542 | + | return x.Last | |||
| 543 | + | } | |||
| 544 | + | return 0 | |||
| 545 | + | } | |||
| 546 | + | ||||
| 547 | + | var File_ermite_storage_merge_request_proto protoreflect.FileDescriptor | |||
| 548 | + | ||||
| 549 | + | const file_ermite_storage_merge_request_proto_rawDesc = "" + | |||
| 550 | + | "\n" + | |||
| 551 | + | "\"ermite/storage/merge_request.proto\x12\x0eermite.storage\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8b\x01\n" + | |||
| 552 | + | "\fRepoSettings\x12I\n" + | |||
| 553 | + | "\x13allowed_merge_modes\x18\x01 \x03(\x0e2\x19.ermite.storage.MergeModeR\x11allowedMergeModes\x120\n" + | |||
| 554 | + | "\x14failing_checks_block\x18\x02 \x01(\bR\x12failingChecksBlock\"\xa6\x04\n" + | |||
| 555 | + | "\fMergeRequest\x12\x16\n" + | |||
| 556 | + | "\x06number\x18\x01 \x01(\x03R\x06number\x12\x14\n" + | |||
| 557 | + | "\x05title\x18\x02 \x01(\tR\x05title\x12 \n" + | |||
| 558 | + | "\vdescription\x18\x03 \x01(\tR\vdescription\x12#\n" + | |||
| 559 | + | "\rsource_branch\x18\x04 \x01(\tR\fsourceBranch\x12#\n" + | |||
| 560 | + | "\rtarget_branch\x18\x05 \x01(\tR\ftargetBranch\x127\n" + | |||
| 561 | + | "\x05state\x18\x06 \x01(\x0e2!.ermite.storage.MergeRequestStateR\x05state\x12\x16\n" + | |||
| 562 | + | "\x06author\x18\a \x01(\tR\x06author\x129\n" + | |||
| 563 | + | "\n" + | |||
| 564 | + | "created_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + | |||
| 565 | + | "\n" + | |||
| 566 | + | "updated_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x17\n" + | |||
| 567 | + | "\ahead_id\x18\n" + | |||
| 568 | + | " \x01(\tR\x06headId\x12\x17\n" + | |||
| 569 | + | "\abase_id\x18\v \x01(\tR\x06baseId\x12(\n" + | |||
| 570 | + | "\x10merged_commit_id\x18\f \x01(\tR\x0emergedCommitId\x128\n" + | |||
| 571 | + | "\n" + | |||
| 572 | + | "merge_mode\x18\r \x01(\x0e2\x19.ermite.storage.MergeModeR\tmergeMode\x12\x1f\n" + | |||
| 573 | + | "\vcomment_seq\x18\x0e \x01(\x03R\n" + | |||
| 574 | + | "commentSeq\"\x8f\x02\n" + | |||
| 575 | + | "\aComment\x12\x0e\n" + | |||
| 576 | + | "\x02id\x18\x01 \x01(\x03R\x02id\x12\x16\n" + | |||
| 577 | + | "\x06author\x18\x02 \x01(\tR\x06author\x12\x12\n" + | |||
| 578 | + | "\x04path\x18\x03 \x01(\tR\x04path\x12\x12\n" + | |||
| 579 | + | "\x04line\x18\x04 \x01(\x05R\x04line\x12,\n" + | |||
| 580 | + | "\x04side\x18\x05 \x01(\x0e2\x18.ermite.storage.DiffSideR\x04side\x12\x1b\n" + | |||
| 581 | + | "\tcommit_id\x18\x06 \x01(\tR\bcommitId\x12\x12\n" + | |||
| 582 | + | "\x04text\x18\a \x01(\tR\x04text\x12\x1a\n" + | |||
| 583 | + | "\bresolved\x18\b \x01(\bR\bresolved\x129\n" + | |||
| 584 | + | "\n" + | |||
| 585 | + | "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"\x1e\n" + | |||
| 586 | + | "\bSequence\x12\x12\n" + | |||
| 587 | + | "\x04last\x18\x01 \x01(\x03R\x04last*[\n" + | |||
| 588 | + | "\tMergeMode\x12\x1a\n" + | |||
| 589 | + | "\x16MERGE_MODE_UNSPECIFIED\x10\x00\x12\x1b\n" + | |||
| 590 | + | "\x17MERGE_MODE_FAST_FORWARD\x10\x01\x12\x15\n" + | |||
| 591 | + | "\x11MERGE_MODE_SQUASH\x10\x02*\x96\x01\n" + | |||
| 592 | + | "\x11MergeRequestState\x12#\n" + | |||
| 593 | + | "\x1fMERGE_REQUEST_STATE_UNSPECIFIED\x10\x00\x12\x1c\n" + | |||
| 594 | + | "\x18MERGE_REQUEST_STATE_OPEN\x10\x01\x12\x1e\n" + | |||
| 595 | + | "\x1aMERGE_REQUEST_STATE_MERGED\x10\x02\x12\x1e\n" + | |||
| 596 | + | "\x1aMERGE_REQUEST_STATE_CLOSED\x10\x03*K\n" + | |||
| 597 | + | "\bDiffSide\x12\x19\n" + | |||
| 598 | + | "\x15DIFF_SIDE_UNSPECIFIED\x10\x00\x12\x11\n" + | |||
| 599 | + | "\rDIFF_SIDE_OLD\x10\x01\x12\x11\n" + | |||
| 600 | + | "\rDIFF_SIDE_NEW\x10\x02B/Z-bnema.dev/ermite/gen/ermite/storage;storagepbb\x06proto3" | |||
| 601 | + | ||||
| 602 | + | var ( | |||
| 603 | + | file_ermite_storage_merge_request_proto_rawDescOnce sync.Once | |||
| 604 | + | file_ermite_storage_merge_request_proto_rawDescData []byte | |||
| 605 | + | ) | |||
| 606 | + | ||||
| 607 | + | func file_ermite_storage_merge_request_proto_rawDescGZIP() []byte { | |||
| 608 | + | file_ermite_storage_merge_request_proto_rawDescOnce.Do(func() { | |||
| 609 | + | file_ermite_storage_merge_request_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ermite_storage_merge_request_proto_rawDesc), len(file_ermite_storage_merge_request_proto_rawDesc))) | |||
| 610 | + | }) | |||
| 611 | + | return file_ermite_storage_merge_request_proto_rawDescData | |||
| 612 | + | } | |||
| 613 | + | ||||
| 614 | + | var file_ermite_storage_merge_request_proto_enumTypes = make([]protoimpl.EnumInfo, 3) | |||
| 615 | + | var file_ermite_storage_merge_request_proto_msgTypes = make([]protoimpl.MessageInfo, 4) | |||
| 616 | + | var file_ermite_storage_merge_request_proto_goTypes = []any{ | |||
| 617 | + | (MergeMode)(0), // 0: ermite.storage.MergeMode | |||
| 618 | + | (MergeRequestState)(0), // 1: ermite.storage.MergeRequestState | |||
| 619 | + | (DiffSide)(0), // 2: ermite.storage.DiffSide | |||
| 620 | + | (*RepoSettings)(nil), // 3: ermite.storage.RepoSettings | |||
| 621 | + | (*MergeRequest)(nil), // 4: ermite.storage.MergeRequest | |||
| 622 | + | (*Comment)(nil), // 5: ermite.storage.Comment | |||
| 623 | + | (*Sequence)(nil), // 6: ermite.storage.Sequence | |||
| 624 | + | (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp | |||
| 625 | + | } | |||
| 626 | + | var file_ermite_storage_merge_request_proto_depIdxs = []int32{ | |||
| 627 | + | 0, // 0: ermite.storage.RepoSettings.allowed_merge_modes:type_name -> ermite.storage.MergeMode | |||
| 628 | + | 1, // 1: ermite.storage.MergeRequest.state:type_name -> ermite.storage.MergeRequestState | |||
| 629 | + | 7, // 2: ermite.storage.MergeRequest.created_at:type_name -> google.protobuf.Timestamp | |||
| 630 | + | 7, // 3: ermite.storage.MergeRequest.updated_at:type_name -> google.protobuf.Timestamp | |||
| 631 | + | 0, // 4: ermite.storage.MergeRequest.merge_mode:type_name -> ermite.storage.MergeMode | |||
| 632 | + | 2, // 5: ermite.storage.Comment.side:type_name -> ermite.storage.DiffSide | |||
| 633 | + | 7, // 6: ermite.storage.Comment.created_at:type_name -> google.protobuf.Timestamp | |||
| 634 | + | 7, // [7:7] is the sub-list for method output_type | |||
| 635 | + | 7, // [7:7] is the sub-list for method input_type | |||
| 636 | + | 7, // [7:7] is the sub-list for extension type_name | |||
| 637 | + | 7, // [7:7] is the sub-list for extension extendee | |||
| 638 | + | 0, // [0:7] is the sub-list for field type_name | |||
| 639 | + | } | |||
| 640 | + | ||||
| 641 | + | func init() { file_ermite_storage_merge_request_proto_init() } | |||
| 642 | + | func file_ermite_storage_merge_request_proto_init() { | |||
| 643 | + | if File_ermite_storage_merge_request_proto != nil { | |||
| 644 | + | return | |||
| 645 | + | } | |||
| 646 | + | type x struct{} | |||
| 647 | + | out := protoimpl.TypeBuilder{ | |||
| 648 | + | File: protoimpl.DescBuilder{ | |||
| 649 | + | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | |||
| 650 | + | RawDescriptor: unsafe.Slice(unsafe.StringData(file_ermite_storage_merge_request_proto_rawDesc), len(file_ermite_storage_merge_request_proto_rawDesc)), | |||
| 651 | + | NumEnums: 3, | |||
| 652 | + | NumMessages: 4, | |||
| 653 | + | NumExtensions: 0, | |||
| 654 | + | NumServices: 0, | |||
| 655 | + | }, | |||
| 656 | + | GoTypes: file_ermite_storage_merge_request_proto_goTypes, | |||
| 657 | + | DependencyIndexes: file_ermite_storage_merge_request_proto_depIdxs, | |||
| 658 | + | EnumInfos: file_ermite_storage_merge_request_proto_enumTypes, | |||
| 659 | + | MessageInfos: file_ermite_storage_merge_request_proto_msgTypes, | |||
| 660 | + | }.Build() | |||
| 661 | + | File_ermite_storage_merge_request_proto = out.File | |||
| 662 | + | file_ermite_storage_merge_request_proto_goTypes = nil | |||
| 663 | + | file_ermite_storage_merge_request_proto_depIdxs = nil | |||
| 664 | + | } | |||
M gen/ermite/storage/repo.pb.go +25 -11 generated file: collapsed
A gen/ermite/storage/signing.pb.go +143 -0 generated file: collapsed
A internal/adapters/kvstore/mergerequests.go +483 -0
| @@ -0,0 +1,483 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package kvstore | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "fmt" | |
| 9 | + | "slices" | |
| 10 | + | "unicode/utf8" | |
| 11 | + | ||
| 12 | + | kv "bnema.dev/ermite-kv" | |
| 13 | + | "google.golang.org/protobuf/proto" | |
| 14 | + | ||
| 15 | + | storagepb "bnema.dev/ermite/gen/ermite/storage" | |
| 16 | + | "bnema.dev/ermite/internal/domain" | |
| 17 | + | "bnema.dev/ermite/internal/ports" | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // Key layout of the merge requests. Repository names hold no '/', so a prefix | |
| 21 | + | // ending in "/" never matches another repository; numbers and comment ids are | |
| 22 | + | // zero-padded so that keys sort numerically. | |
| 23 | + | const ( | |
| 24 | + | mrPrefix = "mr/" // mr/<repo>/<number:010d> | |
| 25 | + | commentPrefix = "mrc/" // mrc/<repo>/<number:010d>/<id:010d> | |
| 26 | + | readPrefix = "mrread/" // mrread/<repo>/<number:010d> | |
| 27 | + | seqPrefix = "mrseq/" // mrseq/<repo> | |
| 28 | + | numberFormat = "%010d" | |
| 29 | + | createTries = 10 | |
| 30 | + | ) | |
| 31 | + | ||
| 32 | + | // MergeRequests is the ermite-kv backed ports.MergeRequestStore. It shares the | |
| 33 | + | // database of the Store it comes from. | |
| 34 | + | type MergeRequests struct { | |
| 35 | + | db *kv.DB | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | var _ ports.MergeRequestStore = (*MergeRequests)(nil) | |
| 39 | + | ||
| 40 | + | // MergeRequests returns the merge request store over the same database. It is | |
| 41 | + | // valid until the Store is closed. | |
| 42 | + | func (s *Store) MergeRequests() *MergeRequests { return &MergeRequests{db: s.db} } | |
| 43 | + | ||
| 44 | + | func mrRepoPrefix(repo domain.RepoName) string { return mrPrefix + repo.String() + "/" } | |
| 45 | + | func mrKey(repo domain.RepoName, n int64) []byte { | |
| 46 | + | return []byte(mrRepoPrefix(repo) + fmt.Sprintf(numberFormat, n)) | |
| 47 | + | } | |
| 48 | + | func commentsPrefix(repo domain.RepoName, n int64) string { | |
| 49 | + | return commentPrefix + repo.String() + "/" + fmt.Sprintf(numberFormat, n) + "/" | |
| 50 | + | } | |
| 51 | + | func commentKey(repo domain.RepoName, n, id int64) []byte { | |
| 52 | + | return []byte(commentsPrefix(repo, n) + fmt.Sprintf(numberFormat, id)) | |
| 53 | + | } | |
| 54 | + | func readKey(repo domain.RepoName, n int64) []byte { | |
| 55 | + | return []byte(readPrefix + repo.String() + "/" + fmt.Sprintf(numberFormat, n)) | |
| 56 | + | } | |
| 57 | + | func seqKey(repo domain.RepoName) []byte { return []byte(seqPrefix + repo.String()) } | |
| 58 | + | ||
| 59 | + | // mergeRequestKeys lists every key that belongs to the repository's merge | |
| 60 | + | // requests. | |
| 61 | + | func (s *Store) mergeRequestKeys(ctx context.Context, repo domain.RepoName) [][]byte { | |
| 62 | + | snap := s.db.Snapshot(ctx) | |
| 63 | + | var keys [][]byte | |
| 64 | + | for _, p := range []string{mrRepoPrefix(repo), commentPrefix + repo.String() + "/", readPrefix + repo.String() + "/"} { | |
| 65 | + | for k := range snap.Prefix(ctx, []byte(p)) { | |
| 66 | + | keys = append(keys, slices.Clone(k)) | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | if _, ok := snap.Get(ctx, seqKey(repo)); ok { | |
| 70 | + | keys = append(keys, seqKey(repo)) | |
| 71 | + | } | |
| 72 | + | return keys | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // Create takes the next number of the repository from mrseq/<repo> and stores | |
| 76 | + | // the merge request under it, in one batch that requires the sequence to be | |
| 77 | + | // unchanged since it was read: two concurrent creations never share a number. | |
| 78 | + | // The batch also requires the repository to be unchanged since it was read, so | |
| 79 | + | // that a repository deleted meanwhile is not left with a stray merge request | |
| 80 | + | // (a push recording pushed_at makes it retry). A missing repository gives | |
| 81 | + | // domain.ErrNotFound. | |
| 82 | + | // | |
| 83 | + | //hexcheck:ignore suspicious-business-logic-in-adapter takes the next number under a version precondition: a storage concern, no policy | |
| 84 | + | func (m *MergeRequests) Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error) { | |
| 85 | + | for range createTries { | |
| 86 | + | repoItem, repoOK := m.db.Get(ctx, repoKey(mr.Repo)) | |
| 87 | + | if !repoOK { | |
| 88 | + | return domain.MergeRequest{}, domain.ErrNotFound | |
| 89 | + | } | |
| 90 | + | var last int64 | |
| 91 | + | item, ok := m.db.Get(ctx, seqKey(mr.Repo)) | |
| 92 | + | if ok { | |
| 93 | + | var seq storagepb.Sequence | |
| 94 | + | if err := proto.Unmarshal(item.Value, &seq); err != nil { | |
| 95 | + | return domain.MergeRequest{}, fmt.Errorf("decode merge request sequence of %q: %w", mr.Repo, err) | |
| 96 | + | } | |
| 97 | + | last = seq.GetLast() | |
| 98 | + | } | |
| 99 | + | mr.Number = last + 1 | |
| 100 | + | val, err := encodeMergeRequest(mr) | |
| 101 | + | if err != nil { | |
| 102 | + | return domain.MergeRequest{}, err | |
| 103 | + | } | |
| 104 | + | var b kv.Batch | |
| 105 | + | b.RequireVersion(repoKey(mr.Repo), repoItem.Version) | |
| 106 | + | if ok { | |
| 107 | + | b.RequireVersion(seqKey(mr.Repo), item.Version) | |
| 108 | + | } else { | |
| 109 | + | b.RequireAbsent(seqKey(mr.Repo)) | |
| 110 | + | } | |
| 111 | + | b.Put(seqKey(mr.Repo), mustMarshal(&storagepb.Sequence{Last: mr.Number})) | |
| 112 | + | b.RequireAbsent(mrKey(mr.Repo, mr.Number)) | |
| 113 | + | b.Put(mrKey(mr.Repo, mr.Number), val) | |
| 114 | + | _, err = m.db.Apply(ctx, &b) | |
| 115 | + | if errors.Is(err, kv.ErrConflict) { | |
| 116 | + | continue | |
| 117 | + | } | |
| 118 | + | if err != nil { | |
| 119 | + | return domain.MergeRequest{}, fmt.Errorf("create merge request of %q: %w", mr.Repo, err) | |
| 120 | + | } | |
| 121 | + | return m.Get(ctx, mr.Repo, mr.Number) | |
| 122 | + | } | |
| 123 | + | return domain.MergeRequest{}, fmt.Errorf("create merge request of %q: %w", mr.Repo, domain.ErrConflict) | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // Get reads mr/<repo>/<number>. | |
| 127 | + | func (m *MergeRequests) Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error) { | |
| 128 | + | if number < 1 { | |
| 129 | + | return domain.MergeRequest{}, domain.ErrNotFound | |
| 130 | + | } | |
| 131 | + | item, ok := m.db.Get(ctx, mrKey(repo, number)) | |
| 132 | + | if !ok { | |
| 133 | + | return domain.MergeRequest{}, domain.ErrNotFound | |
| 134 | + | } | |
| 135 | + | return decodeMergeRequest(repo, item) | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // List returns the merge requests of the repository, highest number first. | |
| 139 | + | func (m *MergeRequests) List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error) { | |
| 140 | + | var out []domain.MergeRequest | |
| 141 | + | for key, item := range m.db.Snapshot(ctx).Prefix(ctx, []byte(mrRepoPrefix(repo))) { | |
| 142 | + | mr, err := decodeMergeRequest(repo, item) | |
| 143 | + | if err != nil { | |
| 144 | + | return nil, fmt.Errorf("decode %s: %w", key, err) | |
| 145 | + | } | |
| 146 | + | out = append(out, mr) | |
| 147 | + | } | |
| 148 | + | slices.Reverse(out) | |
| 149 | + | return out, nil | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | // Update replaces the merge request with a version precondition. | |
| 153 | + | // | |
| 154 | + | //hexcheck:ignore suspicious-business-logic-in-adapter port method name only | |
| 155 | + | func (m *MergeRequests) Update(ctx context.Context, mr domain.MergeRequest) error { | |
| 156 | + | val, err := encodeMergeRequest(mr) | |
| 157 | + | if err != nil { | |
| 158 | + | return err | |
| 159 | + | } | |
| 160 | + | key := mrKey(mr.Repo, mr.Number) | |
| 161 | + | if _, ok := m.db.Get(ctx, key); !ok { | |
| 162 | + | return domain.ErrNotFound | |
| 163 | + | } | |
| 164 | + | if mr.Version == 0 { | |
| 165 | + | return fmt.Errorf("update merge request %d of %q: read it first: %w", mr.Number, mr.Repo, domain.ErrConflict) | |
| 166 | + | } | |
| 167 | + | var b kv.Batch | |
| 168 | + | b.RequireVersion(key, mr.Version) | |
| 169 | + | b.Put(key, val) | |
| 170 | + | if _, err := m.db.Apply(ctx, &b); err != nil { | |
| 171 | + | if errors.Is(err, kv.ErrConflict) { | |
| 172 | + | return domain.ErrConflict | |
| 173 | + | } | |
| 174 | + | return fmt.Errorf("update merge request %d of %q: %w", mr.Number, mr.Repo, err) | |
| 175 | + | } | |
| 176 | + | return nil | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // ListComments returns the comments of the merge request, oldest first. | |
| 180 | + | func (m *MergeRequests) ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error) { | |
| 181 | + | var out []domain.Comment | |
| 182 | + | for key, item := range m.db.Snapshot(ctx).Prefix(ctx, []byte(commentsPrefix(repo, number))) { | |
| 183 | + | c, err := decodeComment(item) | |
| 184 | + | if err != nil { | |
| 185 | + | return nil, fmt.Errorf("decode %s: %w", key, err) | |
| 186 | + | } | |
| 187 | + | out = append(out, c) | |
| 188 | + | } | |
| 189 | + | return out, nil | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | // GetComment reads one comment. | |
| 193 | + | func (m *MergeRequests) GetComment(ctx context.Context, repo domain.RepoName, number, id int64) (domain.Comment, error) { | |
| 194 | + | if number < 1 || id < 1 { | |
| 195 | + | return domain.Comment{}, domain.ErrNotFound | |
| 196 | + | } | |
| 197 | + | item, ok := m.db.Get(ctx, commentKey(repo, number, id)) | |
| 198 | + | if !ok { | |
| 199 | + | return domain.Comment{}, domain.ErrNotFound | |
| 200 | + | } | |
| 201 | + | return decodeComment(item) | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | // SaveComment writes the comment and the merge request in one batch, each with | |
| 205 | + | // its version precondition (absent for a new comment). | |
| 206 | + | func (m *MergeRequests) SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error { | |
| 207 | + | mrVal, err := encodeMergeRequest(mr) | |
| 208 | + | if err != nil { | |
| 209 | + | return err | |
| 210 | + | } | |
| 211 | + | cVal, err := encodeComment(c) | |
| 212 | + | if err != nil { | |
| 213 | + | return err | |
| 214 | + | } | |
| 215 | + | if mr.Version == 0 { | |
| 216 | + | return fmt.Errorf("save comment on %d of %q: read the merge request first: %w", mr.Number, mr.Repo, domain.ErrConflict) | |
| 217 | + | } | |
| 218 | + | var b kv.Batch | |
| 219 | + | b.RequireVersion(mrKey(mr.Repo, mr.Number), mr.Version) | |
| 220 | + | b.Put(mrKey(mr.Repo, mr.Number), mrVal) | |
| 221 | + | if c.Version == 0 { | |
| 222 | + | b.RequireAbsent(commentKey(mr.Repo, mr.Number, c.ID)) | |
| 223 | + | } else { | |
| 224 | + | b.RequireVersion(commentKey(mr.Repo, mr.Number, c.ID), c.Version) | |
| 225 | + | } | |
| 226 | + | b.Put(commentKey(mr.Repo, mr.Number, c.ID), cVal) | |
| 227 | + | if _, err := m.db.Apply(ctx, &b); err != nil { | |
| 228 | + | if errors.Is(err, kv.ErrConflict) { | |
| 229 | + | return domain.ErrConflict | |
| 230 | + | } | |
| 231 | + | return fmt.Errorf("save comment on %d of %q: %w", mr.Number, mr.Repo, err) | |
| 232 | + | } | |
| 233 | + | return nil | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // GetRead reads mrread/<repo>/<number>: the id of a commit, or nothing. | |
| 237 | + | func (m *MergeRequests) GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error) { | |
| 238 | + | item, ok := m.db.Get(ctx, readKey(repo, number)) | |
| 239 | + | if !ok { | |
| 240 | + | return domain.ObjectID{}, nil | |
| 241 | + | } | |
| 242 | + | id, err := domain.ParseObjectID(string(item.Value)) | |
| 243 | + | if err != nil { | |
| 244 | + | return domain.ObjectID{}, fmt.Errorf("decode read position of %d in %q: %w", number, repo, err) | |
| 245 | + | } | |
| 246 | + | return id, nil | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // SetRead writes mrread/<repo>/<number>. | |
| 250 | + | func (m *MergeRequests) SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error { | |
| 251 | + | if head.IsZero() { | |
| 252 | + | return fmt.Errorf("%w: empty read position", domain.ErrInvalidObjectID) | |
| 253 | + | } | |
| 254 | + | var b kv.Batch | |
| 255 | + | b.Put(readKey(repo, number), []byte(head.String())) | |
| 256 | + | if _, err := m.db.Apply(ctx, &b); err != nil { | |
| 257 | + | return fmt.Errorf("record read position of %d in %q: %w", number, repo, err) | |
| 258 | + | } | |
| 259 | + | return nil | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) { | |
| 263 | + | if mr.Repo.IsZero() { | |
| 264 | + | return nil, fmt.Errorf("%w: empty name", domain.ErrInvalidName) | |
| 265 | + | } | |
| 266 | + | if !utf8.ValidString(mr.Author) { | |
| 267 | + | return nil, errors.New("merge request author is not valid UTF-8") | |
| 268 | + | } | |
| 269 | + | state, err := encodeState(mr.State) | |
| 270 | + | if err != nil { | |
| 271 | + | return nil, err | |
| 272 | + | } | |
| 273 | + | mode, err := encodeMode(mr.MergeMode) | |
| 274 | + | if err != nil { | |
| 275 | + | return nil, err | |
| 276 | + | } | |
| 277 | + | return mustMarshal(&storagepb.MergeRequest{ | |
| 278 | + | Number: mr.Number, | |
| 279 | + | Title: mr.Title.String(), | |
| 280 | + | Description: mr.Description.String(), | |
| 281 | + | SourceBranch: mr.Source, | |
| 282 | + | TargetBranch: mr.Target, | |
| 283 | + | State: state, | |
| 284 | + | Author: mr.Author, | |
| 285 | + | CreatedAt: encodeTime(mr.CreatedAt), | |
| 286 | + | UpdatedAt: encodeTime(mr.UpdatedAt), | |
| 287 | + | HeadId: mr.HeadID.String(), | |
| 288 | + | BaseId: mr.BaseID.String(), | |
| 289 | + | MergedCommitId: mr.MergedCommitID.String(), | |
| 290 | + | MergeMode: mode, | |
| 291 | + | CommentSeq: mr.CommentSeq, | |
| 292 | + | }), nil | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | //hexcheck:ignore suspicious-business-logic-in-adapter schema decoding, not a policy | |
| 296 | + | func decodeMergeRequest(repo domain.RepoName, item kv.Item) (domain.MergeRequest, error) { | |
| 297 | + | var pb storagepb.MergeRequest | |
| 298 | + | if err := proto.Unmarshal(item.Value, &pb); err != nil { | |
| 299 | + | return domain.MergeRequest{}, fmt.Errorf("decode merge request: %w", err) | |
| 300 | + | } | |
| 301 | + | fail := func(err error) (domain.MergeRequest, error) { | |
| 302 | + | return domain.MergeRequest{}, fmt.Errorf("decode merge request %d of %q: %w", pb.GetNumber(), repo, err) | |
| 303 | + | } | |
| 304 | + | title, err := domain.ParseMRTitle(pb.GetTitle()) | |
| 305 | + | if err != nil { | |
| 306 | + | return fail(err) | |
| 307 | + | } | |
| 308 | + | desc, err := domain.ParsePlainText(pb.GetDescription()) | |
| 309 | + | if err != nil { | |
| 310 | + | return fail(err) | |
| 311 | + | } | |
| 312 | + | state, err := decodeState(pb.GetState()) | |
| 313 | + | if err != nil { | |
| 314 | + | return fail(err) | |
| 315 | + | } | |
| 316 | + | mr := domain.MergeRequest{ | |
| 317 | + | Repo: repo, Number: pb.GetNumber(), Title: title, Description: desc, | |
| 318 | + | Source: pb.GetSourceBranch(), Target: pb.GetTargetBranch(), State: state, | |
| 319 | + | Author: pb.GetAuthor(), | |
| 320 | + | CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()), | |
| 321 | + | MergeMode: decodeMode(pb.GetMergeMode()), | |
| 322 | + | CommentSeq: pb.GetCommentSeq(), | |
| 323 | + | Version: item.Version, | |
| 324 | + | } | |
| 325 | + | for _, f := range []struct { | |
| 326 | + | dst *domain.ObjectID | |
| 327 | + | src string | |
| 328 | + | }{{&mr.HeadID, pb.GetHeadId()}, {&mr.BaseID, pb.GetBaseId()}, {&mr.MergedCommitID, pb.GetMergedCommitId()}} { | |
| 329 | + | if f.src == "" { | |
| 330 | + | continue | |
| 331 | + | } | |
| 332 | + | if *f.dst, err = domain.ParseObjectID(f.src); err != nil { | |
| 333 | + | return fail(err) | |
| 334 | + | } | |
| 335 | + | } | |
| 336 | + | return mr, nil | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | func encodeComment(c domain.Comment) ([]byte, error) { | |
| 340 | + | side, err := encodeSide(c.Side) | |
| 341 | + | if err != nil { | |
| 342 | + | return nil, err | |
| 343 | + | } | |
| 344 | + | return mustMarshal(&storagepb.Comment{ | |
| 345 | + | Id: c.ID, | |
| 346 | + | Author: c.Author, | |
| 347 | + | Path: c.Path, | |
| 348 | + | Line: int32(c.Line), //nolint:gosec // validated against the diff, far below 2^31 | |
| 349 | + | Side: side, | |
| 350 | + | CommitId: c.CommitID.String(), | |
| 351 | + | Text: c.Text.String(), | |
| 352 | + | Resolved: c.Resolved, | |
| 353 | + | CreatedAt: encodeTime(c.CreatedAt), | |
| 354 | + | }), nil | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | //hexcheck:ignore suspicious-business-logic-in-adapter schema decoding, not a policy | |
| 358 | + | func decodeComment(item kv.Item) (domain.Comment, error) { | |
| 359 | + | var pb storagepb.Comment | |
| 360 | + | if err := proto.Unmarshal(item.Value, &pb); err != nil { | |
| 361 | + | return domain.Comment{}, fmt.Errorf("decode comment: %w", err) | |
| 362 | + | } | |
| 363 | + | text, err := domain.ParsePlainText(pb.GetText()) | |
| 364 | + | if err != nil { | |
| 365 | + | return domain.Comment{}, fmt.Errorf("decode comment %d: %w", pb.GetId(), err) | |
| 366 | + | } | |
| 367 | + | c := domain.Comment{ | |
| 368 | + | ID: pb.GetId(), Author: pb.GetAuthor(), Path: pb.GetPath(), Line: int(pb.GetLine()), | |
| 369 | + | Side: decodeSide(pb.GetSide()), Text: text, Resolved: pb.GetResolved(), | |
| 370 | + | CreatedAt: decodeTime(pb.GetCreatedAt()), Version: item.Version, | |
| 371 | + | } | |
| 372 | + | if pb.GetCommitId() != "" { | |
| 373 | + | if c.CommitID, err = domain.ParseObjectID(pb.GetCommitId()); err != nil { | |
| 374 | + | return domain.Comment{}, fmt.Errorf("decode comment %d: %w", c.ID, err) | |
| 375 | + | } | |
| 376 | + | } | |
| 377 | + | return c, nil | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | func encodeState(s domain.MRState) (storagepb.MergeRequestState, error) { | |
| 381 | + | switch s { | |
| 382 | + | case domain.MRStateOpen: | |
| 383 | + | return storagepb.MergeRequestState_MERGE_REQUEST_STATE_OPEN, nil | |
| 384 | + | case domain.MRStateMerged: | |
| 385 | + | return storagepb.MergeRequestState_MERGE_REQUEST_STATE_MERGED, nil | |
| 386 | + | case domain.MRStateClosed: | |
| 387 | + | return storagepb.MergeRequestState_MERGE_REQUEST_STATE_CLOSED, nil | |
| 388 | + | } | |
| 389 | + | return 0, fmt.Errorf("%w: merge request state %q", domain.ErrInvalid, s) | |
| 390 | + | } | |
| 391 | + | ||
| 392 | + | func decodeState(s storagepb.MergeRequestState) (domain.MRState, error) { | |
| 393 | + | switch s { | |
| 394 | + | case storagepb.MergeRequestState_MERGE_REQUEST_STATE_OPEN: | |
| 395 | + | return domain.MRStateOpen, nil | |
| 396 | + | case storagepb.MergeRequestState_MERGE_REQUEST_STATE_MERGED: | |
| 397 | + | return domain.MRStateMerged, nil | |
| 398 | + | case storagepb.MergeRequestState_MERGE_REQUEST_STATE_CLOSED: | |
| 399 | + | return domain.MRStateClosed, nil | |
| 400 | + | } | |
| 401 | + | return "", fmt.Errorf("unknown merge request state %d", s) | |
| 402 | + | } | |
| 403 | + | ||
| 404 | + | func encodeMode(m domain.MergeMode) (storagepb.MergeMode, error) { | |
| 405 | + | switch m { | |
| 406 | + | case "": | |
| 407 | + | return storagepb.MergeMode_MERGE_MODE_UNSPECIFIED, nil | |
| 408 | + | case domain.MergeModeFastForward: | |
| 409 | + | return storagepb.MergeMode_MERGE_MODE_FAST_FORWARD, nil | |
| 410 | + | case domain.MergeModeSquash: | |
| 411 | + | return storagepb.MergeMode_MERGE_MODE_SQUASH, nil | |
| 412 | + | } | |
| 413 | + | return 0, fmt.Errorf("%w: merge mode %q", domain.ErrInvalid, m) | |
| 414 | + | } | |
| 415 | + | ||
| 416 | + | // decodeMode maps an unknown mode to none: it only labels a merged request. | |
| 417 | + | func decodeMode(m storagepb.MergeMode) domain.MergeMode { | |
| 418 | + | switch m { | |
| 419 | + | case storagepb.MergeMode_MERGE_MODE_FAST_FORWARD: | |
| 420 | + | return domain.MergeModeFastForward | |
| 421 | + | case storagepb.MergeMode_MERGE_MODE_SQUASH: | |
| 422 | + | return domain.MergeModeSquash | |
| 423 | + | } | |
| 424 | + | return "" | |
| 425 | + | } | |
| 426 | + | ||
| 427 | + | func encodeSide(s domain.DiffSide) (storagepb.DiffSide, error) { | |
| 428 | + | switch s { | |
| 429 | + | case domain.SideNone: | |
| 430 | + | return storagepb.DiffSide_DIFF_SIDE_UNSPECIFIED, nil | |
| 431 | + | case domain.SideOld: | |
| 432 | + | return storagepb.DiffSide_DIFF_SIDE_OLD, nil | |
| 433 | + | case domain.SideNew: | |
| 434 | + | return storagepb.DiffSide_DIFF_SIDE_NEW, nil | |
| 435 | + | } | |
| 436 | + | return 0, fmt.Errorf("%w: diff side %q", domain.ErrInvalid, s) | |
| 437 | + | } | |
| 438 | + | ||
| 439 | + | func decodeSide(s storagepb.DiffSide) domain.DiffSide { | |
| 440 | + | switch s { | |
| 441 | + | case storagepb.DiffSide_DIFF_SIDE_OLD: | |
| 442 | + | return domain.SideOld | |
| 443 | + | case storagepb.DiffSide_DIFF_SIDE_NEW: | |
| 444 | + | return domain.SideNew | |
| 445 | + | } | |
| 446 | + | return domain.SideNone | |
| 447 | + | } | |
| 448 | + | ||
| 449 | + | func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) { | |
| 450 | + | pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock} | |
| 451 | + | for _, m := range s.AllowedMergeModes { | |
| 452 | + | mode, err := encodeMode(m) | |
| 453 | + | if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED { | |
| 454 | + | return nil, fmt.Errorf("%w: merge mode %q in the settings", domain.ErrInvalid, m) | |
| 455 | + | } | |
| 456 | + | pb.AllowedMergeModes = append(pb.AllowedMergeModes, mode) | |
| 457 | + | } | |
| 458 | + | return pb, nil | |
| 459 | + | } | |
| 460 | + | ||
| 461 | + | // decodeSettings returns the defaults for a repository stored before merge | |
| 462 | + | // requests existed, and for settings left with no mode that this build knows. | |
| 463 | + | // | |
| 464 | + | //hexcheck:ignore suspicious-business-logic-in-adapter schema decoding with the documented defaults | |
| 465 | + | func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings { | |
| 466 | + | if pb == nil { | |
| 467 | + | return domain.DefaultRepoSettings() | |
| 468 | + | } | |
| 469 | + | var modes []domain.MergeMode | |
| 470 | + | for _, m := range pb.GetAllowedMergeModes() { | |
| 471 | + | if mode := decodeMode(m); mode != "" { | |
| 472 | + | modes = append(modes, mode) | |
| 473 | + | } | |
| 474 | + | } | |
| 475 | + | if len(modes) == 0 { | |
| 476 | + | return domain.DefaultRepoSettings() | |
| 477 | + | } | |
| 478 | + | normalized, err := domain.NormalizeMergeModes(modes) | |
| 479 | + | if err != nil { | |
| 480 | + | return domain.DefaultRepoSettings() | |
| 481 | + | } | |
| 482 | + | return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock()} | |
| 483 | + | } | |
| @@ -0,0 +1,483 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package kvstore | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "errors" | |||
| 8 | + | "fmt" | |||
| 9 | + | "slices" | |||
| 10 | + | "unicode/utf8" | |||
| 11 | + | ||||
| 12 | + | kv "bnema.dev/ermite-kv" | |||
| 13 | + | "google.golang.org/protobuf/proto" | |||
| 14 | + | ||||
| 15 | + | storagepb "bnema.dev/ermite/gen/ermite/storage" | |||
| 16 | + | "bnema.dev/ermite/internal/domain" | |||
| 17 | + | "bnema.dev/ermite/internal/ports" | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | // Key layout of the merge requests. Repository names hold no '/', so a prefix | |||
| 21 | + | // ending in "/" never matches another repository; numbers and comment ids are | |||
| 22 | + | // zero-padded so that keys sort numerically. | |||
| 23 | + | const ( | |||
| 24 | + | mrPrefix = "mr/" // mr/<repo>/<number:010d> | |||
| 25 | + | commentPrefix = "mrc/" // mrc/<repo>/<number:010d>/<id:010d> | |||
| 26 | + | readPrefix = "mrread/" // mrread/<repo>/<number:010d> | |||
| 27 | + | seqPrefix = "mrseq/" // mrseq/<repo> | |||
| 28 | + | numberFormat = "%010d" | |||
| 29 | + | createTries = 10 | |||
| 30 | + | ) | |||
| 31 | + | ||||
| 32 | + | // MergeRequests is the ermite-kv backed ports.MergeRequestStore. It shares the | |||
| 33 | + | // database of the Store it comes from. | |||
| 34 | + | type MergeRequests struct { | |||
| 35 | + | db *kv.DB | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | var _ ports.MergeRequestStore = (*MergeRequests)(nil) | |||
| 39 | + | ||||
| 40 | + | // MergeRequests returns the merge request store over the same database. It is | |||
| 41 | + | // valid until the Store is closed. | |||
| 42 | + | func (s *Store) MergeRequests() *MergeRequests { return &MergeRequests{db: s.db} } | |||
| 43 | + | ||||
| 44 | + | func mrRepoPrefix(repo domain.RepoName) string { return mrPrefix + repo.String() + "/" } | |||
| 45 | + | func mrKey(repo domain.RepoName, n int64) []byte { | |||
| 46 | + | return []byte(mrRepoPrefix(repo) + fmt.Sprintf(numberFormat, n)) | |||
| 47 | + | } | |||
| 48 | + | func commentsPrefix(repo domain.RepoName, n int64) string { | |||
| 49 | + | return commentPrefix + repo.String() + "/" + fmt.Sprintf(numberFormat, n) + "/" | |||
| 50 | + | } | |||
| 51 | + | func commentKey(repo domain.RepoName, n, id int64) []byte { | |||
| 52 | + | return []byte(commentsPrefix(repo, n) + fmt.Sprintf(numberFormat, id)) | |||
| 53 | + | } | |||
| 54 | + | func readKey(repo domain.RepoName, n int64) []byte { | |||
| 55 | + | return []byte(readPrefix + repo.String() + "/" + fmt.Sprintf(numberFormat, n)) | |||
| 56 | + | } | |||
| 57 | + | func seqKey(repo domain.RepoName) []byte { return []byte(seqPrefix + repo.String()) } | |||
| 58 | + | ||||
| 59 | + | // mergeRequestKeys lists every key that belongs to the repository's merge | |||
| 60 | + | // requests. | |||
| 61 | + | func (s *Store) mergeRequestKeys(ctx context.Context, repo domain.RepoName) [][]byte { | |||
| 62 | + | snap := s.db.Snapshot(ctx) | |||
| 63 | + | var keys [][]byte | |||
| 64 | + | for _, p := range []string{mrRepoPrefix(repo), commentPrefix + repo.String() + "/", readPrefix + repo.String() + "/"} { | |||
| 65 | + | for k := range snap.Prefix(ctx, []byte(p)) { | |||
| 66 | + | keys = append(keys, slices.Clone(k)) | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | if _, ok := snap.Get(ctx, seqKey(repo)); ok { | |||
| 70 | + | keys = append(keys, seqKey(repo)) | |||
| 71 | + | } | |||
| 72 | + | return keys | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // Create takes the next number of the repository from mrseq/<repo> and stores | |||
| 76 | + | // the merge request under it, in one batch that requires the sequence to be | |||
| 77 | + | // unchanged since it was read: two concurrent creations never share a number. | |||
| 78 | + | // The batch also requires the repository to be unchanged since it was read, so | |||
| 79 | + | // that a repository deleted meanwhile is not left with a stray merge request | |||
| 80 | + | // (a push recording pushed_at makes it retry). A missing repository gives | |||
| 81 | + | // domain.ErrNotFound. | |||
| 82 | + | // | |||
| 83 | + | //hexcheck:ignore suspicious-business-logic-in-adapter takes the next number under a version precondition: a storage concern, no policy | |||
| 84 | + | func (m *MergeRequests) Create(ctx context.Context, mr domain.MergeRequest) (domain.MergeRequest, error) { | |||
| 85 | + | for range createTries { | |||
| 86 | + | repoItem, repoOK := m.db.Get(ctx, repoKey(mr.Repo)) | |||
| 87 | + | if !repoOK { | |||
| 88 | + | return domain.MergeRequest{}, domain.ErrNotFound | |||
| 89 | + | } | |||
| 90 | + | var last int64 | |||
| 91 | + | item, ok := m.db.Get(ctx, seqKey(mr.Repo)) | |||
| 92 | + | if ok { | |||
| 93 | + | var seq storagepb.Sequence | |||
| 94 | + | if err := proto.Unmarshal(item.Value, &seq); err != nil { | |||
| 95 | + | return domain.MergeRequest{}, fmt.Errorf("decode merge request sequence of %q: %w", mr.Repo, err) | |||
| 96 | + | } | |||
| 97 | + | last = seq.GetLast() | |||
| 98 | + | } | |||
| 99 | + | mr.Number = last + 1 | |||
| 100 | + | val, err := encodeMergeRequest(mr) | |||
| 101 | + | if err != nil { | |||
| 102 | + | return domain.MergeRequest{}, err | |||
| 103 | + | } | |||
| 104 | + | var b kv.Batch | |||
| 105 | + | b.RequireVersion(repoKey(mr.Repo), repoItem.Version) | |||
| 106 | + | if ok { | |||
| 107 | + | b.RequireVersion(seqKey(mr.Repo), item.Version) | |||
| 108 | + | } else { | |||
| 109 | + | b.RequireAbsent(seqKey(mr.Repo)) | |||
| 110 | + | } | |||
| 111 | + | b.Put(seqKey(mr.Repo), mustMarshal(&storagepb.Sequence{Last: mr.Number})) | |||
| 112 | + | b.RequireAbsent(mrKey(mr.Repo, mr.Number)) | |||
| 113 | + | b.Put(mrKey(mr.Repo, mr.Number), val) | |||
| 114 | + | _, err = m.db.Apply(ctx, &b) | |||
| 115 | + | if errors.Is(err, kv.ErrConflict) { | |||
| 116 | + | continue | |||
| 117 | + | } | |||
| 118 | + | if err != nil { | |||
| 119 | + | return domain.MergeRequest{}, fmt.Errorf("create merge request of %q: %w", mr.Repo, err) | |||
| 120 | + | } | |||
| 121 | + | return m.Get(ctx, mr.Repo, mr.Number) | |||
| 122 | + | } | |||
| 123 | + | return domain.MergeRequest{}, fmt.Errorf("create merge request of %q: %w", mr.Repo, domain.ErrConflict) | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | // Get reads mr/<repo>/<number>. | |||
| 127 | + | func (m *MergeRequests) Get(ctx context.Context, repo domain.RepoName, number int64) (domain.MergeRequest, error) { | |||
| 128 | + | if number < 1 { | |||
| 129 | + | return domain.MergeRequest{}, domain.ErrNotFound | |||
| 130 | + | } | |||
| 131 | + | item, ok := m.db.Get(ctx, mrKey(repo, number)) | |||
| 132 | + | if !ok { | |||
| 133 | + | return domain.MergeRequest{}, domain.ErrNotFound | |||
| 134 | + | } | |||
| 135 | + | return decodeMergeRequest(repo, item) | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // List returns the merge requests of the repository, highest number first. | |||
| 139 | + | func (m *MergeRequests) List(ctx context.Context, repo domain.RepoName) ([]domain.MergeRequest, error) { | |||
| 140 | + | var out []domain.MergeRequest | |||
| 141 | + | for key, item := range m.db.Snapshot(ctx).Prefix(ctx, []byte(mrRepoPrefix(repo))) { | |||
| 142 | + | mr, err := decodeMergeRequest(repo, item) | |||
| 143 | + | if err != nil { | |||
| 144 | + | return nil, fmt.Errorf("decode %s: %w", key, err) | |||
| 145 | + | } | |||
| 146 | + | out = append(out, mr) | |||
| 147 | + | } | |||
| 148 | + | slices.Reverse(out) | |||
| 149 | + | return out, nil | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | // Update replaces the merge request with a version precondition. | |||
| 153 | + | // | |||
| 154 | + | //hexcheck:ignore suspicious-business-logic-in-adapter port method name only | |||
| 155 | + | func (m *MergeRequests) Update(ctx context.Context, mr domain.MergeRequest) error { | |||
| 156 | + | val, err := encodeMergeRequest(mr) | |||
| 157 | + | if err != nil { | |||
| 158 | + | return err | |||
| 159 | + | } | |||
| 160 | + | key := mrKey(mr.Repo, mr.Number) | |||
| 161 | + | if _, ok := m.db.Get(ctx, key); !ok { | |||
| 162 | + | return domain.ErrNotFound | |||
| 163 | + | } | |||
| 164 | + | if mr.Version == 0 { | |||
| 165 | + | return fmt.Errorf("update merge request %d of %q: read it first: %w", mr.Number, mr.Repo, domain.ErrConflict) | |||
| 166 | + | } | |||
| 167 | + | var b kv.Batch | |||
| 168 | + | b.RequireVersion(key, mr.Version) | |||
| 169 | + | b.Put(key, val) | |||
| 170 | + | if _, err := m.db.Apply(ctx, &b); err != nil { | |||
| 171 | + | if errors.Is(err, kv.ErrConflict) { | |||
| 172 | + | return domain.ErrConflict | |||
| 173 | + | } | |||
| 174 | + | return fmt.Errorf("update merge request %d of %q: %w", mr.Number, mr.Repo, err) | |||
| 175 | + | } | |||
| 176 | + | return nil | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // ListComments returns the comments of the merge request, oldest first. | |||
| 180 | + | func (m *MergeRequests) ListComments(ctx context.Context, repo domain.RepoName, number int64) ([]domain.Comment, error) { | |||
| 181 | + | var out []domain.Comment | |||
| 182 | + | for key, item := range m.db.Snapshot(ctx).Prefix(ctx, []byte(commentsPrefix(repo, number))) { | |||
| 183 | + | c, err := decodeComment(item) | |||
| 184 | + | if err != nil { | |||
| 185 | + | return nil, fmt.Errorf("decode %s: %w", key, err) | |||
| 186 | + | } | |||
| 187 | + | out = append(out, c) | |||
| 188 | + | } | |||
| 189 | + | return out, nil | |||
| 190 | + | } | |||
| 191 | + | ||||
| 192 | + | // GetComment reads one comment. | |||
| 193 | + | func (m *MergeRequests) GetComment(ctx context.Context, repo domain.RepoName, number, id int64) (domain.Comment, error) { | |||
| 194 | + | if number < 1 || id < 1 { | |||
| 195 | + | return domain.Comment{}, domain.ErrNotFound | |||
| 196 | + | } | |||
| 197 | + | item, ok := m.db.Get(ctx, commentKey(repo, number, id)) | |||
| 198 | + | if !ok { | |||
| 199 | + | return domain.Comment{}, domain.ErrNotFound | |||
| 200 | + | } | |||
| 201 | + | return decodeComment(item) | |||
| 202 | + | } | |||
| 203 | + | ||||
| 204 | + | // SaveComment writes the comment and the merge request in one batch, each with | |||
| 205 | + | // its version precondition (absent for a new comment). | |||
| 206 | + | func (m *MergeRequests) SaveComment(ctx context.Context, mr domain.MergeRequest, c domain.Comment) error { | |||
| 207 | + | mrVal, err := encodeMergeRequest(mr) | |||
| 208 | + | if err != nil { | |||
| 209 | + | return err | |||
| 210 | + | } | |||
| 211 | + | cVal, err := encodeComment(c) | |||
| 212 | + | if err != nil { | |||
| 213 | + | return err | |||
| 214 | + | } | |||
| 215 | + | if mr.Version == 0 { | |||
| 216 | + | return fmt.Errorf("save comment on %d of %q: read the merge request first: %w", mr.Number, mr.Repo, domain.ErrConflict) | |||
| 217 | + | } | |||
| 218 | + | var b kv.Batch | |||
| 219 | + | b.RequireVersion(mrKey(mr.Repo, mr.Number), mr.Version) | |||
| 220 | + | b.Put(mrKey(mr.Repo, mr.Number), mrVal) | |||
| 221 | + | if c.Version == 0 { | |||
| 222 | + | b.RequireAbsent(commentKey(mr.Repo, mr.Number, c.ID)) | |||
| 223 | + | } else { | |||
| 224 | + | b.RequireVersion(commentKey(mr.Repo, mr.Number, c.ID), c.Version) | |||
| 225 | + | } | |||
| 226 | + | b.Put(commentKey(mr.Repo, mr.Number, c.ID), cVal) | |||
| 227 | + | if _, err := m.db.Apply(ctx, &b); err != nil { | |||
| 228 | + | if errors.Is(err, kv.ErrConflict) { | |||
| 229 | + | return domain.ErrConflict | |||
| 230 | + | } | |||
| 231 | + | return fmt.Errorf("save comment on %d of %q: %w", mr.Number, mr.Repo, err) | |||
| 232 | + | } | |||
| 233 | + | return nil | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // GetRead reads mrread/<repo>/<number>: the id of a commit, or nothing. | |||
| 237 | + | func (m *MergeRequests) GetRead(ctx context.Context, repo domain.RepoName, number int64) (domain.ObjectID, error) { | |||
| 238 | + | item, ok := m.db.Get(ctx, readKey(repo, number)) | |||
| 239 | + | if !ok { | |||
| 240 | + | return domain.ObjectID{}, nil | |||
| 241 | + | } | |||
| 242 | + | id, err := domain.ParseObjectID(string(item.Value)) | |||
| 243 | + | if err != nil { | |||
| 244 | + | return domain.ObjectID{}, fmt.Errorf("decode read position of %d in %q: %w", number, repo, err) | |||
| 245 | + | } | |||
| 246 | + | return id, nil | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | // SetRead writes mrread/<repo>/<number>. | |||
| 250 | + | func (m *MergeRequests) SetRead(ctx context.Context, repo domain.RepoName, number int64, head domain.ObjectID) error { | |||
| 251 | + | if head.IsZero() { | |||
| 252 | + | return fmt.Errorf("%w: empty read position", domain.ErrInvalidObjectID) | |||
| 253 | + | } | |||
| 254 | + | var b kv.Batch | |||
| 255 | + | b.Put(readKey(repo, number), []byte(head.String())) | |||
| 256 | + | if _, err := m.db.Apply(ctx, &b); err != nil { | |||
| 257 | + | return fmt.Errorf("record read position of %d in %q: %w", number, repo, err) | |||
| 258 | + | } | |||
| 259 | + | return nil | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | func encodeMergeRequest(mr domain.MergeRequest) ([]byte, error) { | |||
| 263 | + | if mr.Repo.IsZero() { | |||
| 264 | + | return nil, fmt.Errorf("%w: empty name", domain.ErrInvalidName) | |||
| 265 | + | } | |||
| 266 | + | if !utf8.ValidString(mr.Author) { | |||
| 267 | + | return nil, errors.New("merge request author is not valid UTF-8") | |||
| 268 | + | } | |||
| 269 | + | state, err := encodeState(mr.State) | |||
| 270 | + | if err != nil { | |||
| 271 | + | return nil, err | |||
| 272 | + | } | |||
| 273 | + | mode, err := encodeMode(mr.MergeMode) | |||
| 274 | + | if err != nil { | |||
| 275 | + | return nil, err | |||
| 276 | + | } | |||
| 277 | + | return mustMarshal(&storagepb.MergeRequest{ | |||
| 278 | + | Number: mr.Number, | |||
| 279 | + | Title: mr.Title.String(), | |||
| 280 | + | Description: mr.Description.String(), | |||
| 281 | + | SourceBranch: mr.Source, | |||
| 282 | + | TargetBranch: mr.Target, | |||
| 283 | + | State: state, | |||
| 284 | + | Author: mr.Author, | |||
| 285 | + | CreatedAt: encodeTime(mr.CreatedAt), | |||
| 286 | + | UpdatedAt: encodeTime(mr.UpdatedAt), | |||
| 287 | + | HeadId: mr.HeadID.String(), | |||
| 288 | + | BaseId: mr.BaseID.String(), | |||
| 289 | + | MergedCommitId: mr.MergedCommitID.String(), | |||
| 290 | + | MergeMode: mode, | |||
| 291 | + | CommentSeq: mr.CommentSeq, | |||
| 292 | + | }), nil | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | //hexcheck:ignore suspicious-business-logic-in-adapter schema decoding, not a policy | |||
| 296 | + | func decodeMergeRequest(repo domain.RepoName, item kv.Item) (domain.MergeRequest, error) { | |||
| 297 | + | var pb storagepb.MergeRequest | |||
| 298 | + | if err := proto.Unmarshal(item.Value, &pb); err != nil { | |||
| 299 | + | return domain.MergeRequest{}, fmt.Errorf("decode merge request: %w", err) | |||
| 300 | + | } | |||
| 301 | + | fail := func(err error) (domain.MergeRequest, error) { | |||
| 302 | + | return domain.MergeRequest{}, fmt.Errorf("decode merge request %d of %q: %w", pb.GetNumber(), repo, err) | |||
| 303 | + | } | |||
| 304 | + | title, err := domain.ParseMRTitle(pb.GetTitle()) | |||
| 305 | + | if err != nil { | |||
| 306 | + | return fail(err) | |||
| 307 | + | } | |||
| 308 | + | desc, err := domain.ParsePlainText(pb.GetDescription()) | |||
| 309 | + | if err != nil { | |||
| 310 | + | return fail(err) | |||
| 311 | + | } | |||
| 312 | + | state, err := decodeState(pb.GetState()) | |||
| 313 | + | if err != nil { | |||
| 314 | + | return fail(err) | |||
| 315 | + | } | |||
| 316 | + | mr := domain.MergeRequest{ | |||
| 317 | + | Repo: repo, Number: pb.GetNumber(), Title: title, Description: desc, | |||
| 318 | + | Source: pb.GetSourceBranch(), Target: pb.GetTargetBranch(), State: state, | |||
| 319 | + | Author: pb.GetAuthor(), | |||
| 320 | + | CreatedAt: decodeTime(pb.GetCreatedAt()), UpdatedAt: decodeTime(pb.GetUpdatedAt()), | |||
| 321 | + | MergeMode: decodeMode(pb.GetMergeMode()), | |||
| 322 | + | CommentSeq: pb.GetCommentSeq(), | |||
| 323 | + | Version: item.Version, | |||
| 324 | + | } | |||
| 325 | + | for _, f := range []struct { | |||
| 326 | + | dst *domain.ObjectID | |||
| 327 | + | src string | |||
| 328 | + | }{{&mr.HeadID, pb.GetHeadId()}, {&mr.BaseID, pb.GetBaseId()}, {&mr.MergedCommitID, pb.GetMergedCommitId()}} { | |||
| 329 | + | if f.src == "" { | |||
| 330 | + | continue | |||
| 331 | + | } | |||
| 332 | + | if *f.dst, err = domain.ParseObjectID(f.src); err != nil { | |||
| 333 | + | return fail(err) | |||
| 334 | + | } | |||
| 335 | + | } | |||
| 336 | + | return mr, nil | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | func encodeComment(c domain.Comment) ([]byte, error) { | |||
| 340 | + | side, err := encodeSide(c.Side) | |||
| 341 | + | if err != nil { | |||
| 342 | + | return nil, err | |||
| 343 | + | } | |||
| 344 | + | return mustMarshal(&storagepb.Comment{ | |||
| 345 | + | Id: c.ID, | |||
| 346 | + | Author: c.Author, | |||
| 347 | + | Path: c.Path, | |||
| 348 | + | Line: int32(c.Line), //nolint:gosec // validated against the diff, far below 2^31 | |||
| 349 | + | Side: side, | |||
| 350 | + | CommitId: c.CommitID.String(), | |||
| 351 | + | Text: c.Text.String(), | |||
| 352 | + | Resolved: c.Resolved, | |||
| 353 | + | CreatedAt: encodeTime(c.CreatedAt), | |||
| 354 | + | }), nil | |||
| 355 | + | } | |||
| 356 | + | ||||
| 357 | + | //hexcheck:ignore suspicious-business-logic-in-adapter schema decoding, not a policy | |||
| 358 | + | func decodeComment(item kv.Item) (domain.Comment, error) { | |||
| 359 | + | var pb storagepb.Comment | |||
| 360 | + | if err := proto.Unmarshal(item.Value, &pb); err != nil { | |||
| 361 | + | return domain.Comment{}, fmt.Errorf("decode comment: %w", err) | |||
| 362 | + | } | |||
| 363 | + | text, err := domain.ParsePlainText(pb.GetText()) | |||
| 364 | + | if err != nil { | |||
| 365 | + | return domain.Comment{}, fmt.Errorf("decode comment %d: %w", pb.GetId(), err) | |||
| 366 | + | } | |||
| 367 | + | c := domain.Comment{ | |||
| 368 | + | ID: pb.GetId(), Author: pb.GetAuthor(), Path: pb.GetPath(), Line: int(pb.GetLine()), | |||
| 369 | + | Side: decodeSide(pb.GetSide()), Text: text, Resolved: pb.GetResolved(), | |||
| 370 | + | CreatedAt: decodeTime(pb.GetCreatedAt()), Version: item.Version, | |||
| 371 | + | } | |||
| 372 | + | if pb.GetCommitId() != "" { | |||
| 373 | + | if c.CommitID, err = domain.ParseObjectID(pb.GetCommitId()); err != nil { | |||
| 374 | + | return domain.Comment{}, fmt.Errorf("decode comment %d: %w", c.ID, err) | |||
| 375 | + | } | |||
| 376 | + | } | |||
| 377 | + | return c, nil | |||
| 378 | + | } | |||
| 379 | + | ||||
| 380 | + | func encodeState(s domain.MRState) (storagepb.MergeRequestState, error) { | |||
| 381 | + | switch s { | |||
| 382 | + | case domain.MRStateOpen: | |||
| 383 | + | return storagepb.MergeRequestState_MERGE_REQUEST_STATE_OPEN, nil | |||
| 384 | + | case domain.MRStateMerged: | |||
| 385 | + | return storagepb.MergeRequestState_MERGE_REQUEST_STATE_MERGED, nil | |||
| 386 | + | case domain.MRStateClosed: | |||
| 387 | + | return storagepb.MergeRequestState_MERGE_REQUEST_STATE_CLOSED, nil | |||
| 388 | + | } | |||
| 389 | + | return 0, fmt.Errorf("%w: merge request state %q", domain.ErrInvalid, s) | |||
| 390 | + | } | |||
| 391 | + | ||||
| 392 | + | func decodeState(s storagepb.MergeRequestState) (domain.MRState, error) { | |||
| 393 | + | switch s { | |||
| 394 | + | case storagepb.MergeRequestState_MERGE_REQUEST_STATE_OPEN: | |||
| 395 | + | return domain.MRStateOpen, nil | |||
| 396 | + | case storagepb.MergeRequestState_MERGE_REQUEST_STATE_MERGED: | |||
| 397 | + | return domain.MRStateMerged, nil | |||
| 398 | + | case storagepb.MergeRequestState_MERGE_REQUEST_STATE_CLOSED: | |||
| 399 | + | return domain.MRStateClosed, nil | |||
| 400 | + | } | |||
| 401 | + | return "", fmt.Errorf("unknown merge request state %d", s) | |||
| 402 | + | } | |||
| 403 | + | ||||
| 404 | + | func encodeMode(m domain.MergeMode) (storagepb.MergeMode, error) { | |||
| 405 | + | switch m { | |||
| 406 | + | case "": | |||
| 407 | + | return storagepb.MergeMode_MERGE_MODE_UNSPECIFIED, nil | |||
| 408 | + | case domain.MergeModeFastForward: | |||
| 409 | + | return storagepb.MergeMode_MERGE_MODE_FAST_FORWARD, nil | |||
| 410 | + | case domain.MergeModeSquash: | |||
| 411 | + | return storagepb.MergeMode_MERGE_MODE_SQUASH, nil | |||
| 412 | + | } | |||
| 413 | + | return 0, fmt.Errorf("%w: merge mode %q", domain.ErrInvalid, m) | |||
| 414 | + | } | |||
| 415 | + | ||||
| 416 | + | // decodeMode maps an unknown mode to none: it only labels a merged request. | |||
| 417 | + | func decodeMode(m storagepb.MergeMode) domain.MergeMode { | |||
| 418 | + | switch m { | |||
| 419 | + | case storagepb.MergeMode_MERGE_MODE_FAST_FORWARD: | |||
| 420 | + | return domain.MergeModeFastForward | |||
| 421 | + | case storagepb.MergeMode_MERGE_MODE_SQUASH: | |||
| 422 | + | return domain.MergeModeSquash | |||
| 423 | + | } | |||
| 424 | + | return "" | |||
| 425 | + | } | |||
| 426 | + | ||||
| 427 | + | func encodeSide(s domain.DiffSide) (storagepb.DiffSide, error) { | |||
| 428 | + | switch s { | |||
| 429 | + | case domain.SideNone: | |||
| 430 | + | return storagepb.DiffSide_DIFF_SIDE_UNSPECIFIED, nil | |||
| 431 | + | case domain.SideOld: | |||
| 432 | + | return storagepb.DiffSide_DIFF_SIDE_OLD, nil | |||
| 433 | + | case domain.SideNew: | |||
| 434 | + | return storagepb.DiffSide_DIFF_SIDE_NEW, nil | |||
| 435 | + | } | |||
| 436 | + | return 0, fmt.Errorf("%w: diff side %q", domain.ErrInvalid, s) | |||
| 437 | + | } | |||
| 438 | + | ||||
| 439 | + | func decodeSide(s storagepb.DiffSide) domain.DiffSide { | |||
| 440 | + | switch s { | |||
| 441 | + | case storagepb.DiffSide_DIFF_SIDE_OLD: | |||
| 442 | + | return domain.SideOld | |||
| 443 | + | case storagepb.DiffSide_DIFF_SIDE_NEW: | |||
| 444 | + | return domain.SideNew | |||
| 445 | + | } | |||
| 446 | + | return domain.SideNone | |||
| 447 | + | } | |||
| 448 | + | ||||
| 449 | + | func encodeSettings(s domain.RepoSettings) (*storagepb.RepoSettings, error) { | |||
| 450 | + | pb := &storagepb.RepoSettings{FailingChecksBlock: s.FailingChecksBlock} | |||
| 451 | + | for _, m := range s.AllowedMergeModes { | |||
| 452 | + | mode, err := encodeMode(m) | |||
| 453 | + | if err != nil || mode == storagepb.MergeMode_MERGE_MODE_UNSPECIFIED { | |||
| 454 | + | return nil, fmt.Errorf("%w: merge mode %q in the settings", domain.ErrInvalid, m) | |||
| 455 | + | } | |||
| 456 | + | pb.AllowedMergeModes = append(pb.AllowedMergeModes, mode) | |||
| 457 | + | } | |||
| 458 | + | return pb, nil | |||
| 459 | + | } | |||
| 460 | + | ||||
| 461 | + | // decodeSettings returns the defaults for a repository stored before merge | |||
| 462 | + | // requests existed, and for settings left with no mode that this build knows. | |||
| 463 | + | // | |||
| 464 | + | //hexcheck:ignore suspicious-business-logic-in-adapter schema decoding with the documented defaults | |||
| 465 | + | func decodeSettings(pb *storagepb.RepoSettings) domain.RepoSettings { | |||
| 466 | + | if pb == nil { | |||
| 467 | + | return domain.DefaultRepoSettings() | |||
| 468 | + | } | |||
| 469 | + | var modes []domain.MergeMode | |||
| 470 | + | for _, m := range pb.GetAllowedMergeModes() { | |||
| 471 | + | if mode := decodeMode(m); mode != "" { | |||
| 472 | + | modes = append(modes, mode) | |||
| 473 | + | } | |||
| 474 | + | } | |||
| 475 | + | if len(modes) == 0 { | |||
| 476 | + | return domain.DefaultRepoSettings() | |||
| 477 | + | } | |||
| 478 | + | normalized, err := domain.NormalizeMergeModes(modes) | |||
| 479 | + | if err != nil { | |||
| 480 | + | return domain.DefaultRepoSettings() | |||
| 481 | + | } | |||
| 482 | + | return domain.RepoSettings{AllowedMergeModes: normalized, FailingChecksBlock: pb.GetFailingChecksBlock()} | |||
| 483 | + | } | |||
A internal/adapters/kvstore/mergerequests_test.go +455 -0
| @@ -0,0 +1,455 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package kvstore_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "strings" | |
| 8 | + | "sync" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "github.com/stretchr/testify/assert" | |
| 13 | + | "github.com/stretchr/testify/require" | |
| 14 | + | "google.golang.org/protobuf/proto" | |
| 15 | + | ||
| 16 | + | kv "bnema.dev/ermite-kv" | |
| 17 | + | ||
| 18 | + | storagepb "bnema.dev/ermite/gen/ermite/storage" | |
| 19 | + | "bnema.dev/ermite/internal/adapters/kvstore" | |
| 20 | + | "bnema.dev/ermite/internal/domain" | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | func newMR(t *testing.T, repo domain.RepoName, title string) domain.MergeRequest { | |
| 24 | + | t.Helper() | |
| 25 | + | ti, err := domain.ParseMRTitle(title) | |
| 26 | + | require.NoError(t, err) | |
| 27 | + | desc, err := domain.ParsePlainText("why\nand how") | |
| 28 | + | require.NoError(t, err) | |
| 29 | + | return domain.MergeRequest{ | |
| 30 | + | Repo: repo, Title: ti, Description: desc, Source: "feat/x", Target: "main", | |
| 31 | + | State: domain.MRStateOpen, Author: "agent", CreatedAt: created, UpdatedAt: created, | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | func oid(c byte) domain.ObjectID { | |
| 36 | + | id, err := domain.ParseObjectID(strings.Repeat(string(c), 40)) | |
| 37 | + | if err != nil { | |
| 38 | + | panic(err) | |
| 39 | + | } | |
| 40 | + | return id | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // repoWithMRs opens a store holding repository "proj". | |
| 44 | + | func repoWithMRs(t *testing.T) (*kvstore.Store, *kvstore.MergeRequests, domain.RepoName) { | |
| 45 | + | t.Helper() | |
| 46 | + | s := openStore(t, t.TempDir()) | |
| 47 | + | n := name(t, "proj") | |
| 48 | + | require.NoError(t, s.Create(context.Background(), domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: created})) | |
| 49 | + | return s, s.MergeRequests(), n | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func TestMergeRequestNumbersAreSequentialPerRepository(t *testing.T) { | |
| 53 | + | ctx := context.Background() | |
| 54 | + | s, mrs, n := repoWithMRs(t) | |
| 55 | + | other := name(t, "other") | |
| 56 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: other, CreatedAt: created, Visibility: domain.VisibilityPublic})) | |
| 57 | + | ||
| 58 | + | for want := int64(1); want <= 3; want++ { | |
| 59 | + | got, err := mrs.Create(ctx, newMR(t, n, "t")) | |
| 60 | + | require.NoError(t, err) | |
| 61 | + | assert.Equal(t, want, got.Number) | |
| 62 | + | } | |
| 63 | + | got, err := mrs.Create(ctx, newMR(t, other, "t")) | |
| 64 | + | require.NoError(t, err) | |
| 65 | + | assert.Equal(t, int64(1), got.Number, "each repository counts on its own") | |
| 66 | + | ||
| 67 | + | _, err = mrs.Create(ctx, newMR(t, name(t, "ghost"), "t")) | |
| 68 | + | assert.ErrorIs(t, err, domain.ErrNotFound, "no merge request for a missing repository") | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func TestMergeRequestRoundTrip(t *testing.T) { | |
| 72 | + | ctx := context.Background() | |
| 73 | + | _, mrs, n := repoWithMRs(t) | |
| 74 | + | in := newMR(t, n, "Ajoute « ça »") | |
| 75 | + | in.HeadID, in.BaseID, in.MergedCommitID = oid('a'), oid('b'), oid('c') | |
| 76 | + | in.MergeMode = domain.MergeModeSquash | |
| 77 | + | in.State = domain.MRStateMerged | |
| 78 | + | in.CommentSeq = 4 | |
| 79 | + | created1, err := mrs.Create(ctx, in) | |
| 80 | + | require.NoError(t, err) | |
| 81 | + | ||
| 82 | + | got, err := mrs.Get(ctx, n, created1.Number) | |
| 83 | + | require.NoError(t, err) | |
| 84 | + | assert.NotZero(t, got.Version) | |
| 85 | + | in.Number, in.Version = 1, got.Version | |
| 86 | + | assert.Equal(t, in, got) | |
| 87 | + | ||
| 88 | + | _, err = mrs.Get(ctx, n, 2) | |
| 89 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 90 | + | _, err = mrs.Get(ctx, n, 0) | |
| 91 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 92 | + | _, err = mrs.Get(ctx, n, -3) | |
| 93 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) { | |
| 97 | + | ctx := context.Background() | |
| 98 | + | s, mrs, n := repoWithMRs(t) | |
| 99 | + | // "proj-2" shares the prefix "proj": its keys must not leak into proj's. | |
| 100 | + | sib := name(t, "proj-2") | |
| 101 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: sib, CreatedAt: created, Visibility: domain.VisibilityPublic})) | |
| 102 | + | for range 12 { // beyond 10: numbers sort numerically, not as text | |
| 103 | + | _, err := mrs.Create(ctx, newMR(t, n, "t")) | |
| 104 | + | require.NoError(t, err) | |
| 105 | + | } | |
| 106 | + | _, err := mrs.Create(ctx, newMR(t, sib, "sibling")) | |
| 107 | + | require.NoError(t, err) | |
| 108 | + | ||
| 109 | + | list, err := mrs.List(ctx, n) | |
| 110 | + | require.NoError(t, err) | |
| 111 | + | require.Len(t, list, 12) | |
| 112 | + | for i, mr := range list { | |
| 113 | + | assert.Equal(t, int64(12-i), mr.Number) | |
| 114 | + | assert.Equal(t, n, mr.Repo) | |
| 115 | + | } | |
| 116 | + | list, err = mrs.List(ctx, name(t, "none")) | |
| 117 | + | require.NoError(t, err) | |
| 118 | + | assert.Empty(t, list) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | func TestConcurrentCreateNeverSharesANumber(t *testing.T) { | |
| 122 | + | ctx := context.Background() | |
| 123 | + | _, mrs, n := repoWithMRs(t) | |
| 124 | + | const workers = 8 | |
| 125 | + | var ( | |
| 126 | + | wg sync.WaitGroup | |
| 127 | + | mu sync.Mutex | |
| 128 | + | numbers []int64 | |
| 129 | + | ) | |
| 130 | + | for range workers { | |
| 131 | + | wg.Go(func() { | |
| 132 | + | got, err := mrs.Create(ctx, newMR(t, n, "t")) | |
| 133 | + | if assert.NoError(t, err) { | |
| 134 | + | mu.Lock() | |
| 135 | + | numbers = append(numbers, got.Number) | |
| 136 | + | mu.Unlock() | |
| 137 | + | } | |
| 138 | + | }) | |
| 139 | + | } | |
| 140 | + | wg.Wait() | |
| 141 | + | assert.ElementsMatch(t, []int64{1, 2, 3, 4, 5, 6, 7, 8}, numbers) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | func TestMergeRequestUpdateNeedsTheVersionItWasReadAt(t *testing.T) { | |
| 145 | + | ctx := context.Background() | |
| 146 | + | _, mrs, n := repoWithMRs(t) | |
| 147 | + | first, err := mrs.Create(ctx, newMR(t, n, "t")) | |
| 148 | + | require.NoError(t, err) | |
| 149 | + | ||
| 150 | + | a, err := mrs.Get(ctx, n, first.Number) | |
| 151 | + | require.NoError(t, err) | |
| 152 | + | b := a | |
| 153 | + | a.State = domain.MRStateClosed | |
| 154 | + | require.NoError(t, mrs.Update(ctx, a)) | |
| 155 | + | ||
| 156 | + | b.State = domain.MRStateMerged | |
| 157 | + | assert.ErrorIs(t, mrs.Update(ctx, b), domain.ErrConflict, "b was read before a wrote") | |
| 158 | + | ||
| 159 | + | got, err := mrs.Get(ctx, n, first.Number) | |
| 160 | + | require.NoError(t, err) | |
| 161 | + | assert.Equal(t, domain.MRStateClosed, got.State) | |
| 162 | + | ||
| 163 | + | gone := got | |
| 164 | + | gone.Number = 99 | |
| 165 | + | assert.ErrorIs(t, mrs.Update(ctx, gone), domain.ErrNotFound) | |
| 166 | + | fresh := newMR(t, n, "never read") | |
| 167 | + | fresh.Number = first.Number | |
| 168 | + | assert.ErrorIs(t, mrs.Update(ctx, fresh), domain.ErrConflict, "an unread request has no version") | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func newComment(t *testing.T, id int64, text string) domain.Comment { | |
| 172 | + | t.Helper() | |
| 173 | + | pt, err := domain.ParsePlainText(text) | |
| 174 | + | require.NoError(t, err) | |
| 175 | + | return domain.Comment{ | |
| 176 | + | ID: id, Author: "root", Path: "a.txt", Line: 3, Side: domain.SideNew, | |
| 177 | + | CommitID: oid('d'), Text: pt, CreatedAt: created.Add(time.Duration(id) * time.Minute), | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | func TestComments(t *testing.T) { | |
| 182 | + | ctx := context.Background() | |
| 183 | + | _, mrs, n := repoWithMRs(t) | |
| 184 | + | first, err := mrs.Create(ctx, newMR(t, n, "t")) | |
| 185 | + | require.NoError(t, err) | |
| 186 | + | ||
| 187 | + | // Twelve comments: ids sort numerically too. | |
| 188 | + | for id := int64(1); id <= 12; id++ { | |
| 189 | + | mr, err := mrs.Get(ctx, n, first.Number) | |
| 190 | + | require.NoError(t, err) | |
| 191 | + | mr.CommentSeq = id | |
| 192 | + | c := newComment(t, id, "note") | |
| 193 | + | if id == 2 { | |
| 194 | + | c.Path, c.Line, c.Side = "", 0, domain.SideNone | |
| 195 | + | } | |
| 196 | + | require.NoError(t, mrs.SaveComment(ctx, mr, c)) | |
| 197 | + | } | |
| 198 | + | list, err := mrs.ListComments(ctx, n, first.Number) | |
| 199 | + | require.NoError(t, err) | |
| 200 | + | require.Len(t, list, 12) | |
| 201 | + | for i, c := range list { | |
| 202 | + | assert.Equal(t, int64(i+1), c.ID, "oldest first") | |
| 203 | + | } | |
| 204 | + | assert.True(t, list[1].IsGeneral()) | |
| 205 | + | assert.Equal(t, domain.SideNone, list[1].Side) | |
| 206 | + | assert.Equal(t, domain.SideNew, list[0].Side) | |
| 207 | + | ||
| 208 | + | mr, err := mrs.Get(ctx, n, first.Number) | |
| 209 | + | require.NoError(t, err) | |
| 210 | + | assert.Equal(t, int64(12), mr.CommentSeq) | |
| 211 | + | ||
| 212 | + | c, err := mrs.GetComment(ctx, n, first.Number, 3) | |
| 213 | + | require.NoError(t, err) | |
| 214 | + | assert.NotZero(t, c.Version) | |
| 215 | + | assert.Equal(t, "note", c.Text.String()) | |
| 216 | + | assert.Equal(t, oid('d'), c.CommitID) | |
| 217 | + | for _, id := range []int64{0, 13, -1} { | |
| 218 | + | _, err = mrs.GetComment(ctx, n, first.Number, id) | |
| 219 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | // Another merge request's comments are not listed. | |
| 223 | + | second, err := mrs.Create(ctx, newMR(t, n, "t2")) | |
| 224 | + | require.NoError(t, err) | |
| 225 | + | list, err = mrs.ListComments(ctx, n, second.Number) | |
| 226 | + | require.NoError(t, err) | |
| 227 | + | assert.Empty(t, list) | |
| 228 | + | } | |
| 229 | + | ||
| 230 | + | func TestSaveCommentPreconditions(t *testing.T) { | |
| 231 | + | ctx := context.Background() | |
| 232 | + | _, mrs, n := repoWithMRs(t) | |
| 233 | + | first, err := mrs.Create(ctx, newMR(t, n, "t")) | |
| 234 | + | require.NoError(t, err) | |
| 235 | + | mr, err := mrs.Get(ctx, n, first.Number) | |
| 236 | + | require.NoError(t, err) | |
| 237 | + | ||
| 238 | + | mr.CommentSeq = 1 | |
| 239 | + | require.NoError(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "one"))) | |
| 240 | + | ||
| 241 | + | // The same id again, from the same stale read: the request moved on. | |
| 242 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "dup")), domain.ErrConflict) | |
| 243 | + | ||
| 244 | + | // A fresh read of the request but a new comment with an existing id. | |
| 245 | + | mr, err = mrs.Get(ctx, n, first.Number) | |
| 246 | + | require.NoError(t, err) | |
| 247 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "dup")), domain.ErrConflict, "a new comment must not overwrite") | |
| 248 | + | ||
| 249 | + | // Resolving needs the comment version. | |
| 250 | + | c, err := mrs.GetComment(ctx, n, first.Number, 1) | |
| 251 | + | require.NoError(t, err) | |
| 252 | + | stale := c | |
| 253 | + | c.Resolved = true | |
| 254 | + | require.NoError(t, mrs.SaveComment(ctx, mr, c)) | |
| 255 | + | mr, err = mrs.Get(ctx, n, first.Number) | |
| 256 | + | require.NoError(t, err) | |
| 257 | + | stale.Resolved = false | |
| 258 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, mr, stale), domain.ErrConflict) | |
| 259 | + | ||
| 260 | + | // An unread request cannot be saved with. | |
| 261 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, newMR(t, n, "x"), newComment(t, 2, "n")), domain.ErrConflict) | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | func TestReadPosition(t *testing.T) { | |
| 265 | + | ctx := context.Background() | |
| 266 | + | _, mrs, n := repoWithMRs(t) | |
| 267 | + | got, err := mrs.GetRead(ctx, n, 1) | |
| 268 | + | require.NoError(t, err) | |
| 269 | + | assert.True(t, got.IsZero()) | |
| 270 | + | ||
| 271 | + | require.NoError(t, mrs.SetRead(ctx, n, 1, oid('a'))) | |
| 272 | + | require.NoError(t, mrs.SetRead(ctx, n, 1, oid('b'))) | |
| 273 | + | got, err = mrs.GetRead(ctx, n, 1) | |
| 274 | + | require.NoError(t, err) | |
| 275 | + | assert.Equal(t, oid('b'), got) | |
| 276 | + | got, err = mrs.GetRead(ctx, n, 2) | |
| 277 | + | require.NoError(t, err) | |
| 278 | + | assert.True(t, got.IsZero()) | |
| 279 | + | assert.Error(t, mrs.SetRead(ctx, n, 1, domain.ObjectID{})) | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | // TestDeleteDropsTheMergeRequestsOfTheRepository: deleting a repository drops | |
| 283 | + | // its merge requests, comments, read positions and number sequence, and only | |
| 284 | + | // its own; a repository created again under the name starts at !1. | |
| 285 | + | func TestDeleteDropsTheMergeRequestsOfTheRepository(t *testing.T) { | |
| 286 | + | ctx := context.Background() | |
| 287 | + | s, mrs, n := repoWithMRs(t) | |
| 288 | + | sib := name(t, "proj-2") | |
| 289 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: sib, CreatedAt: created, Visibility: domain.VisibilityPublic})) | |
| 290 | + | ||
| 291 | + | for _, repo := range []domain.RepoName{n, sib} { | |
| 292 | + | mr, err := mrs.Create(ctx, newMR(t, repo, "t")) | |
| 293 | + | require.NoError(t, err) | |
| 294 | + | mr.CommentSeq = 1 | |
| 295 | + | require.NoError(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "note"))) | |
| 296 | + | require.NoError(t, mrs.SetRead(ctx, repo, mr.Number, oid('a'))) | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | require.NoError(t, s.Delete(ctx, n)) | |
| 300 | + | ||
| 301 | + | list, err := mrs.List(ctx, n) | |
| 302 | + | require.NoError(t, err) | |
| 303 | + | assert.Empty(t, list) | |
| 304 | + | comments, err := mrs.ListComments(ctx, n, 1) | |
| 305 | + | require.NoError(t, err) | |
| 306 | + | assert.Empty(t, comments) | |
| 307 | + | read, err := mrs.GetRead(ctx, n, 1) | |
| 308 | + | require.NoError(t, err) | |
| 309 | + | assert.True(t, read.IsZero()) | |
| 310 | + | ||
| 311 | + | list, err = mrs.List(ctx, sib) | |
| 312 | + | require.NoError(t, err) | |
| 313 | + | require.Len(t, list, 1, "a repository whose name starts like the deleted one keeps its requests") | |
| 314 | + | comments, err = mrs.ListComments(ctx, sib, 1) | |
| 315 | + | require.NoError(t, err) | |
| 316 | + | assert.Len(t, comments, 1) | |
| 317 | + | ||
| 318 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate})) | |
| 319 | + | again, err := mrs.Create(ctx, newMR(t, n, "fresh")) | |
| 320 | + | require.NoError(t, err) | |
| 321 | + | assert.Equal(t, int64(1), again.Number) | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | func TestDeleteManyMergeRequests(t *testing.T) { | |
| 325 | + | ctx := context.Background() | |
| 326 | + | s, mrs, n := repoWithMRs(t) | |
| 327 | + | for range 1100 { | |
| 328 | + | _, err := mrs.Create(ctx, newMR(t, n, "t")) | |
| 329 | + | require.NoError(t, err) | |
| 330 | + | } | |
| 331 | + | require.NoError(t, s.Delete(ctx, n)) | |
| 332 | + | list, err := mrs.List(ctx, n) | |
| 333 | + | require.NoError(t, err) | |
| 334 | + | assert.Empty(t, list) | |
| 335 | + | _, err = s.Get(ctx, n) | |
| 336 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | func TestRepoSettings(t *testing.T) { | |
| 340 | + | ctx := context.Background() | |
| 341 | + | s := openStore(t, t.TempDir()) | |
| 342 | + | n := name(t, "proj") | |
| 343 | + | ||
| 344 | + | // Created without settings: they are stored explicitly, with the defaults. | |
| 345 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate})) | |
| 346 | + | got, err := s.Get(ctx, n) | |
| 347 | + | require.NoError(t, err) | |
| 348 | + | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | |
| 349 | + | ||
| 350 | + | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false} | |
| 351 | + | require.NoError(t, s.Update(ctx, got)) | |
| 352 | + | again, err := s.Get(ctx, n) | |
| 353 | + | require.NoError(t, err) | |
| 354 | + | assert.Equal(t, got.Settings, again.Settings) | |
| 355 | + | ||
| 356 | + | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{"rebase"}} | |
| 357 | + | assert.ErrorIs(t, s.Update(ctx, got), domain.ErrInvalid) | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before | |
| 361 | + | // merge requests existed has no settings field. | |
| 362 | + | func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) { | |
| 363 | + | ctx := context.Background() | |
| 364 | + | dir := t.TempDir() | |
| 365 | + | s, err := kvstore.Open(ctx, dir) | |
| 366 | + | require.NoError(t, err) | |
| 367 | + | require.NoError(t, s.Close(ctx)) | |
| 368 | + | ||
| 369 | + | // Write a legacy value straight into the database. | |
| 370 | + | db, err := kv.Open(ctx, dir, kv.Options{}) | |
| 371 | + | require.NoError(t, err) | |
| 372 | + | legacy, err := proto.Marshal(&storagepb.Repo{Name: "legacy", Visibility: storagepb.Visibility_VISIBILITY_PUBLIC}) | |
| 373 | + | require.NoError(t, err) | |
| 374 | + | var b kv.Batch | |
| 375 | + | b.Put([]byte("repo/legacy"), legacy) | |
| 376 | + | _, err = db.Apply(ctx, &b) | |
| 377 | + | require.NoError(t, err) | |
| 378 | + | require.NoError(t, db.Close(ctx)) | |
| 379 | + | ||
| 380 | + | s = openStore(t, dir) | |
| 381 | + | got, err := s.Get(ctx, name(t, "legacy")) | |
| 382 | + | require.NoError(t, err) | |
| 383 | + | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | |
| 384 | + | list, err := s.List(ctx) | |
| 385 | + | require.NoError(t, err) | |
| 386 | + | require.Len(t, list, 1) | |
| 387 | + | assert.Equal(t, domain.DefaultRepoSettings(), list[0].Settings) | |
| 388 | + | } | |
| 389 | + | ||
| 390 | + | func TestSigningSeed(t *testing.T) { | |
| 391 | + | ctx := context.Background() | |
| 392 | + | dir := t.TempDir() | |
| 393 | + | s := openStore(t, dir) | |
| 394 | + | calls := 0 | |
| 395 | + | gen := func() ([]byte, error) { | |
| 396 | + | calls++ | |
| 397 | + | return []byte(strings.Repeat(string(rune('a'+calls)), kvstore.SigningSeedLen)), nil | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | first, err := s.SigningSeed(ctx, gen, created) | |
| 401 | + | require.NoError(t, err) | |
| 402 | + | require.Len(t, first, kvstore.SigningSeedLen) | |
| 403 | + | second, err := s.SigningSeed(ctx, gen, created) | |
| 404 | + | require.NoError(t, err) | |
| 405 | + | assert.Equal(t, first, second, "generated once") | |
| 406 | + | assert.Equal(t, 1, calls) | |
| 407 | + | ||
| 408 | + | // It survives a restart. | |
| 409 | + | require.NoError(t, s.Close(ctx)) | |
| 410 | + | s2 := openStore(t, dir) | |
| 411 | + | third, err := s2.SigningSeed(ctx, gen, created) | |
| 412 | + | require.NoError(t, err) | |
| 413 | + | assert.Equal(t, first, third) | |
| 414 | + | assert.Equal(t, 1, calls) | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | func TestSigningSeedConcurrentFirstStart(t *testing.T) { | |
| 418 | + | ctx := context.Background() | |
| 419 | + | s := openStore(t, t.TempDir()) | |
| 420 | + | var ( | |
| 421 | + | wg sync.WaitGroup | |
| 422 | + | mu sync.Mutex | |
| 423 | + | seeds [][]byte | |
| 424 | + | n byte | |
| 425 | + | ) | |
| 426 | + | for range 6 { | |
| 427 | + | wg.Go(func() { | |
| 428 | + | seed, err := s.SigningSeed(ctx, func() ([]byte, error) { | |
| 429 | + | mu.Lock() | |
| 430 | + | defer mu.Unlock() | |
| 431 | + | n++ | |
| 432 | + | return []byte(strings.Repeat(string(rune('A'+n)), kvstore.SigningSeedLen)), nil | |
| 433 | + | }, created) | |
| 434 | + | if assert.NoError(t, err) { | |
| 435 | + | mu.Lock() | |
| 436 | + | seeds = append(seeds, seed) | |
| 437 | + | mu.Unlock() | |
| 438 | + | } | |
| 439 | + | }) | |
| 440 | + | } | |
| 441 | + | wg.Wait() | |
| 442 | + | for _, seed := range seeds { | |
| 443 | + | assert.Equal(t, seeds[0], seed, "every caller gets the winner's seed") | |
| 444 | + | } | |
| 445 | + | } | |
| 446 | + | ||
| 447 | + | func TestSigningSeedRejectsBadGenerators(t *testing.T) { | |
| 448 | + | ctx := context.Background() | |
| 449 | + | s := openStore(t, t.TempDir()) | |
| 450 | + | _, err := s.SigningSeed(ctx, func() ([]byte, error) { return []byte("short"), nil }, created) | |
| 451 | + | require.Error(t, err) | |
| 452 | + | assert.NotContains(t, err.Error(), "short") | |
| 453 | + | _, err = s.SigningSeed(ctx, func() ([]byte, error) { return nil, assert.AnError }, created) | |
| 454 | + | require.ErrorIs(t, err, assert.AnError) | |
| 455 | + | } | |
| @@ -0,0 +1,455 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package kvstore_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "strings" | |||
| 8 | + | "sync" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ||||
| 12 | + | "github.com/stretchr/testify/assert" | |||
| 13 | + | "github.com/stretchr/testify/require" | |||
| 14 | + | "google.golang.org/protobuf/proto" | |||
| 15 | + | ||||
| 16 | + | kv "bnema.dev/ermite-kv" | |||
| 17 | + | ||||
| 18 | + | storagepb "bnema.dev/ermite/gen/ermite/storage" | |||
| 19 | + | "bnema.dev/ermite/internal/adapters/kvstore" | |||
| 20 | + | "bnema.dev/ermite/internal/domain" | |||
| 21 | + | ) | |||
| 22 | + | ||||
| 23 | + | func newMR(t *testing.T, repo domain.RepoName, title string) domain.MergeRequest { | |||
| 24 | + | t.Helper() | |||
| 25 | + | ti, err := domain.ParseMRTitle(title) | |||
| 26 | + | require.NoError(t, err) | |||
| 27 | + | desc, err := domain.ParsePlainText("why\nand how") | |||
| 28 | + | require.NoError(t, err) | |||
| 29 | + | return domain.MergeRequest{ | |||
| 30 | + | Repo: repo, Title: ti, Description: desc, Source: "feat/x", Target: "main", | |||
| 31 | + | State: domain.MRStateOpen, Author: "agent", CreatedAt: created, UpdatedAt: created, | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | func oid(c byte) domain.ObjectID { | |||
| 36 | + | id, err := domain.ParseObjectID(strings.Repeat(string(c), 40)) | |||
| 37 | + | if err != nil { | |||
| 38 | + | panic(err) | |||
| 39 | + | } | |||
| 40 | + | return id | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // repoWithMRs opens a store holding repository "proj". | |||
| 44 | + | func repoWithMRs(t *testing.T) (*kvstore.Store, *kvstore.MergeRequests, domain.RepoName) { | |||
| 45 | + | t.Helper() | |||
| 46 | + | s := openStore(t, t.TempDir()) | |||
| 47 | + | n := name(t, "proj") | |||
| 48 | + | require.NoError(t, s.Create(context.Background(), domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: created})) | |||
| 49 | + | return s, s.MergeRequests(), n | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func TestMergeRequestNumbersAreSequentialPerRepository(t *testing.T) { | |||
| 53 | + | ctx := context.Background() | |||
| 54 | + | s, mrs, n := repoWithMRs(t) | |||
| 55 | + | other := name(t, "other") | |||
| 56 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: other, CreatedAt: created, Visibility: domain.VisibilityPublic})) | |||
| 57 | + | ||||
| 58 | + | for want := int64(1); want <= 3; want++ { | |||
| 59 | + | got, err := mrs.Create(ctx, newMR(t, n, "t")) | |||
| 60 | + | require.NoError(t, err) | |||
| 61 | + | assert.Equal(t, want, got.Number) | |||
| 62 | + | } | |||
| 63 | + | got, err := mrs.Create(ctx, newMR(t, other, "t")) | |||
| 64 | + | require.NoError(t, err) | |||
| 65 | + | assert.Equal(t, int64(1), got.Number, "each repository counts on its own") | |||
| 66 | + | ||||
| 67 | + | _, err = mrs.Create(ctx, newMR(t, name(t, "ghost"), "t")) | |||
| 68 | + | assert.ErrorIs(t, err, domain.ErrNotFound, "no merge request for a missing repository") | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func TestMergeRequestRoundTrip(t *testing.T) { | |||
| 72 | + | ctx := context.Background() | |||
| 73 | + | _, mrs, n := repoWithMRs(t) | |||
| 74 | + | in := newMR(t, n, "Ajoute « ça »") | |||
| 75 | + | in.HeadID, in.BaseID, in.MergedCommitID = oid('a'), oid('b'), oid('c') | |||
| 76 | + | in.MergeMode = domain.MergeModeSquash | |||
| 77 | + | in.State = domain.MRStateMerged | |||
| 78 | + | in.CommentSeq = 4 | |||
| 79 | + | created1, err := mrs.Create(ctx, in) | |||
| 80 | + | require.NoError(t, err) | |||
| 81 | + | ||||
| 82 | + | got, err := mrs.Get(ctx, n, created1.Number) | |||
| 83 | + | require.NoError(t, err) | |||
| 84 | + | assert.NotZero(t, got.Version) | |||
| 85 | + | in.Number, in.Version = 1, got.Version | |||
| 86 | + | assert.Equal(t, in, got) | |||
| 87 | + | ||||
| 88 | + | _, err = mrs.Get(ctx, n, 2) | |||
| 89 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |||
| 90 | + | _, err = mrs.Get(ctx, n, 0) | |||
| 91 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |||
| 92 | + | _, err = mrs.Get(ctx, n, -3) | |||
| 93 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | func TestMergeRequestListIsNewestFirstAndPerRepository(t *testing.T) { | |||
| 97 | + | ctx := context.Background() | |||
| 98 | + | s, mrs, n := repoWithMRs(t) | |||
| 99 | + | // "proj-2" shares the prefix "proj": its keys must not leak into proj's. | |||
| 100 | + | sib := name(t, "proj-2") | |||
| 101 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: sib, CreatedAt: created, Visibility: domain.VisibilityPublic})) | |||
| 102 | + | for range 12 { // beyond 10: numbers sort numerically, not as text | |||
| 103 | + | _, err := mrs.Create(ctx, newMR(t, n, "t")) | |||
| 104 | + | require.NoError(t, err) | |||
| 105 | + | } | |||
| 106 | + | _, err := mrs.Create(ctx, newMR(t, sib, "sibling")) | |||
| 107 | + | require.NoError(t, err) | |||
| 108 | + | ||||
| 109 | + | list, err := mrs.List(ctx, n) | |||
| 110 | + | require.NoError(t, err) | |||
| 111 | + | require.Len(t, list, 12) | |||
| 112 | + | for i, mr := range list { | |||
| 113 | + | assert.Equal(t, int64(12-i), mr.Number) | |||
| 114 | + | assert.Equal(t, n, mr.Repo) | |||
| 115 | + | } | |||
| 116 | + | list, err = mrs.List(ctx, name(t, "none")) | |||
| 117 | + | require.NoError(t, err) | |||
| 118 | + | assert.Empty(t, list) | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | func TestConcurrentCreateNeverSharesANumber(t *testing.T) { | |||
| 122 | + | ctx := context.Background() | |||
| 123 | + | _, mrs, n := repoWithMRs(t) | |||
| 124 | + | const workers = 8 | |||
| 125 | + | var ( | |||
| 126 | + | wg sync.WaitGroup | |||
| 127 | + | mu sync.Mutex | |||
| 128 | + | numbers []int64 | |||
| 129 | + | ) | |||
| 130 | + | for range workers { | |||
| 131 | + | wg.Go(func() { | |||
| 132 | + | got, err := mrs.Create(ctx, newMR(t, n, "t")) | |||
| 133 | + | if assert.NoError(t, err) { | |||
| 134 | + | mu.Lock() | |||
| 135 | + | numbers = append(numbers, got.Number) | |||
| 136 | + | mu.Unlock() | |||
| 137 | + | } | |||
| 138 | + | }) | |||
| 139 | + | } | |||
| 140 | + | wg.Wait() | |||
| 141 | + | assert.ElementsMatch(t, []int64{1, 2, 3, 4, 5, 6, 7, 8}, numbers) | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | func TestMergeRequestUpdateNeedsTheVersionItWasReadAt(t *testing.T) { | |||
| 145 | + | ctx := context.Background() | |||
| 146 | + | _, mrs, n := repoWithMRs(t) | |||
| 147 | + | first, err := mrs.Create(ctx, newMR(t, n, "t")) | |||
| 148 | + | require.NoError(t, err) | |||
| 149 | + | ||||
| 150 | + | a, err := mrs.Get(ctx, n, first.Number) | |||
| 151 | + | require.NoError(t, err) | |||
| 152 | + | b := a | |||
| 153 | + | a.State = domain.MRStateClosed | |||
| 154 | + | require.NoError(t, mrs.Update(ctx, a)) | |||
| 155 | + | ||||
| 156 | + | b.State = domain.MRStateMerged | |||
| 157 | + | assert.ErrorIs(t, mrs.Update(ctx, b), domain.ErrConflict, "b was read before a wrote") | |||
| 158 | + | ||||
| 159 | + | got, err := mrs.Get(ctx, n, first.Number) | |||
| 160 | + | require.NoError(t, err) | |||
| 161 | + | assert.Equal(t, domain.MRStateClosed, got.State) | |||
| 162 | + | ||||
| 163 | + | gone := got | |||
| 164 | + | gone.Number = 99 | |||
| 165 | + | assert.ErrorIs(t, mrs.Update(ctx, gone), domain.ErrNotFound) | |||
| 166 | + | fresh := newMR(t, n, "never read") | |||
| 167 | + | fresh.Number = first.Number | |||
| 168 | + | assert.ErrorIs(t, mrs.Update(ctx, fresh), domain.ErrConflict, "an unread request has no version") | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func newComment(t *testing.T, id int64, text string) domain.Comment { | |||
| 172 | + | t.Helper() | |||
| 173 | + | pt, err := domain.ParsePlainText(text) | |||
| 174 | + | require.NoError(t, err) | |||
| 175 | + | return domain.Comment{ | |||
| 176 | + | ID: id, Author: "root", Path: "a.txt", Line: 3, Side: domain.SideNew, | |||
| 177 | + | CommitID: oid('d'), Text: pt, CreatedAt: created.Add(time.Duration(id) * time.Minute), | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | func TestComments(t *testing.T) { | |||
| 182 | + | ctx := context.Background() | |||
| 183 | + | _, mrs, n := repoWithMRs(t) | |||
| 184 | + | first, err := mrs.Create(ctx, newMR(t, n, "t")) | |||
| 185 | + | require.NoError(t, err) | |||
| 186 | + | ||||
| 187 | + | // Twelve comments: ids sort numerically too. | |||
| 188 | + | for id := int64(1); id <= 12; id++ { | |||
| 189 | + | mr, err := mrs.Get(ctx, n, first.Number) | |||
| 190 | + | require.NoError(t, err) | |||
| 191 | + | mr.CommentSeq = id | |||
| 192 | + | c := newComment(t, id, "note") | |||
| 193 | + | if id == 2 { | |||
| 194 | + | c.Path, c.Line, c.Side = "", 0, domain.SideNone | |||
| 195 | + | } | |||
| 196 | + | require.NoError(t, mrs.SaveComment(ctx, mr, c)) | |||
| 197 | + | } | |||
| 198 | + | list, err := mrs.ListComments(ctx, n, first.Number) | |||
| 199 | + | require.NoError(t, err) | |||
| 200 | + | require.Len(t, list, 12) | |||
| 201 | + | for i, c := range list { | |||
| 202 | + | assert.Equal(t, int64(i+1), c.ID, "oldest first") | |||
| 203 | + | } | |||
| 204 | + | assert.True(t, list[1].IsGeneral()) | |||
| 205 | + | assert.Equal(t, domain.SideNone, list[1].Side) | |||
| 206 | + | assert.Equal(t, domain.SideNew, list[0].Side) | |||
| 207 | + | ||||
| 208 | + | mr, err := mrs.Get(ctx, n, first.Number) | |||
| 209 | + | require.NoError(t, err) | |||
| 210 | + | assert.Equal(t, int64(12), mr.CommentSeq) | |||
| 211 | + | ||||
| 212 | + | c, err := mrs.GetComment(ctx, n, first.Number, 3) | |||
| 213 | + | require.NoError(t, err) | |||
| 214 | + | assert.NotZero(t, c.Version) | |||
| 215 | + | assert.Equal(t, "note", c.Text.String()) | |||
| 216 | + | assert.Equal(t, oid('d'), c.CommitID) | |||
| 217 | + | for _, id := range []int64{0, 13, -1} { | |||
| 218 | + | _, err = mrs.GetComment(ctx, n, first.Number, id) | |||
| 219 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | // Another merge request's comments are not listed. | |||
| 223 | + | second, err := mrs.Create(ctx, newMR(t, n, "t2")) | |||
| 224 | + | require.NoError(t, err) | |||
| 225 | + | list, err = mrs.ListComments(ctx, n, second.Number) | |||
| 226 | + | require.NoError(t, err) | |||
| 227 | + | assert.Empty(t, list) | |||
| 228 | + | } | |||
| 229 | + | ||||
| 230 | + | func TestSaveCommentPreconditions(t *testing.T) { | |||
| 231 | + | ctx := context.Background() | |||
| 232 | + | _, mrs, n := repoWithMRs(t) | |||
| 233 | + | first, err := mrs.Create(ctx, newMR(t, n, "t")) | |||
| 234 | + | require.NoError(t, err) | |||
| 235 | + | mr, err := mrs.Get(ctx, n, first.Number) | |||
| 236 | + | require.NoError(t, err) | |||
| 237 | + | ||||
| 238 | + | mr.CommentSeq = 1 | |||
| 239 | + | require.NoError(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "one"))) | |||
| 240 | + | ||||
| 241 | + | // The same id again, from the same stale read: the request moved on. | |||
| 242 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "dup")), domain.ErrConflict) | |||
| 243 | + | ||||
| 244 | + | // A fresh read of the request but a new comment with an existing id. | |||
| 245 | + | mr, err = mrs.Get(ctx, n, first.Number) | |||
| 246 | + | require.NoError(t, err) | |||
| 247 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "dup")), domain.ErrConflict, "a new comment must not overwrite") | |||
| 248 | + | ||||
| 249 | + | // Resolving needs the comment version. | |||
| 250 | + | c, err := mrs.GetComment(ctx, n, first.Number, 1) | |||
| 251 | + | require.NoError(t, err) | |||
| 252 | + | stale := c | |||
| 253 | + | c.Resolved = true | |||
| 254 | + | require.NoError(t, mrs.SaveComment(ctx, mr, c)) | |||
| 255 | + | mr, err = mrs.Get(ctx, n, first.Number) | |||
| 256 | + | require.NoError(t, err) | |||
| 257 | + | stale.Resolved = false | |||
| 258 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, mr, stale), domain.ErrConflict) | |||
| 259 | + | ||||
| 260 | + | // An unread request cannot be saved with. | |||
| 261 | + | assert.ErrorIs(t, mrs.SaveComment(ctx, newMR(t, n, "x"), newComment(t, 2, "n")), domain.ErrConflict) | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | func TestReadPosition(t *testing.T) { | |||
| 265 | + | ctx := context.Background() | |||
| 266 | + | _, mrs, n := repoWithMRs(t) | |||
| 267 | + | got, err := mrs.GetRead(ctx, n, 1) | |||
| 268 | + | require.NoError(t, err) | |||
| 269 | + | assert.True(t, got.IsZero()) | |||
| 270 | + | ||||
| 271 | + | require.NoError(t, mrs.SetRead(ctx, n, 1, oid('a'))) | |||
| 272 | + | require.NoError(t, mrs.SetRead(ctx, n, 1, oid('b'))) | |||
| 273 | + | got, err = mrs.GetRead(ctx, n, 1) | |||
| 274 | + | require.NoError(t, err) | |||
| 275 | + | assert.Equal(t, oid('b'), got) | |||
| 276 | + | got, err = mrs.GetRead(ctx, n, 2) | |||
| 277 | + | require.NoError(t, err) | |||
| 278 | + | assert.True(t, got.IsZero()) | |||
| 279 | + | assert.Error(t, mrs.SetRead(ctx, n, 1, domain.ObjectID{})) | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | // TestDeleteDropsTheMergeRequestsOfTheRepository: deleting a repository drops | |||
| 283 | + | // its merge requests, comments, read positions and number sequence, and only | |||
| 284 | + | // its own; a repository created again under the name starts at !1. | |||
| 285 | + | func TestDeleteDropsTheMergeRequestsOfTheRepository(t *testing.T) { | |||
| 286 | + | ctx := context.Background() | |||
| 287 | + | s, mrs, n := repoWithMRs(t) | |||
| 288 | + | sib := name(t, "proj-2") | |||
| 289 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: sib, CreatedAt: created, Visibility: domain.VisibilityPublic})) | |||
| 290 | + | ||||
| 291 | + | for _, repo := range []domain.RepoName{n, sib} { | |||
| 292 | + | mr, err := mrs.Create(ctx, newMR(t, repo, "t")) | |||
| 293 | + | require.NoError(t, err) | |||
| 294 | + | mr.CommentSeq = 1 | |||
| 295 | + | require.NoError(t, mrs.SaveComment(ctx, mr, newComment(t, 1, "note"))) | |||
| 296 | + | require.NoError(t, mrs.SetRead(ctx, repo, mr.Number, oid('a'))) | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | require.NoError(t, s.Delete(ctx, n)) | |||
| 300 | + | ||||
| 301 | + | list, err := mrs.List(ctx, n) | |||
| 302 | + | require.NoError(t, err) | |||
| 303 | + | assert.Empty(t, list) | |||
| 304 | + | comments, err := mrs.ListComments(ctx, n, 1) | |||
| 305 | + | require.NoError(t, err) | |||
| 306 | + | assert.Empty(t, comments) | |||
| 307 | + | read, err := mrs.GetRead(ctx, n, 1) | |||
| 308 | + | require.NoError(t, err) | |||
| 309 | + | assert.True(t, read.IsZero()) | |||
| 310 | + | ||||
| 311 | + | list, err = mrs.List(ctx, sib) | |||
| 312 | + | require.NoError(t, err) | |||
| 313 | + | require.Len(t, list, 1, "a repository whose name starts like the deleted one keeps its requests") | |||
| 314 | + | comments, err = mrs.ListComments(ctx, sib, 1) | |||
| 315 | + | require.NoError(t, err) | |||
| 316 | + | assert.Len(t, comments, 1) | |||
| 317 | + | ||||
| 318 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate})) | |||
| 319 | + | again, err := mrs.Create(ctx, newMR(t, n, "fresh")) | |||
| 320 | + | require.NoError(t, err) | |||
| 321 | + | assert.Equal(t, int64(1), again.Number) | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | func TestDeleteManyMergeRequests(t *testing.T) { | |||
| 325 | + | ctx := context.Background() | |||
| 326 | + | s, mrs, n := repoWithMRs(t) | |||
| 327 | + | for range 1100 { | |||
| 328 | + | _, err := mrs.Create(ctx, newMR(t, n, "t")) | |||
| 329 | + | require.NoError(t, err) | |||
| 330 | + | } | |||
| 331 | + | require.NoError(t, s.Delete(ctx, n)) | |||
| 332 | + | list, err := mrs.List(ctx, n) | |||
| 333 | + | require.NoError(t, err) | |||
| 334 | + | assert.Empty(t, list) | |||
| 335 | + | _, err = s.Get(ctx, n) | |||
| 336 | + | assert.ErrorIs(t, err, domain.ErrNotFound) | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | func TestRepoSettings(t *testing.T) { | |||
| 340 | + | ctx := context.Background() | |||
| 341 | + | s := openStore(t, t.TempDir()) | |||
| 342 | + | n := name(t, "proj") | |||
| 343 | + | ||||
| 344 | + | // Created without settings: they are stored explicitly, with the defaults. | |||
| 345 | + | require.NoError(t, s.Create(ctx, domain.Repo{Name: n, CreatedAt: created, Visibility: domain.VisibilityPrivate})) | |||
| 346 | + | got, err := s.Get(ctx, n) | |||
| 347 | + | require.NoError(t, err) | |||
| 348 | + | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | |||
| 349 | + | ||||
| 350 | + | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{domain.MergeModeSquash}, FailingChecksBlock: false} | |||
| 351 | + | require.NoError(t, s.Update(ctx, got)) | |||
| 352 | + | again, err := s.Get(ctx, n) | |||
| 353 | + | require.NoError(t, err) | |||
| 354 | + | assert.Equal(t, got.Settings, again.Settings) | |||
| 355 | + | ||||
| 356 | + | got.Settings = domain.RepoSettings{AllowedMergeModes: []domain.MergeMode{"rebase"}} | |||
| 357 | + | assert.ErrorIs(t, s.Update(ctx, got), domain.ErrInvalid) | |||
| 358 | + | } | |||
| 359 | + | ||||
| 360 | + | // TestRepoWithoutStoredSettingsReadsTheDefaults: a repository stored before | |||
| 361 | + | // merge requests existed has no settings field. | |||
| 362 | + | func TestRepoWithoutStoredSettingsReadsTheDefaults(t *testing.T) { | |||
| 363 | + | ctx := context.Background() | |||
| 364 | + | dir := t.TempDir() | |||
| 365 | + | s, err := kvstore.Open(ctx, dir) | |||
| 366 | + | require.NoError(t, err) | |||
| 367 | + | require.NoError(t, s.Close(ctx)) | |||
| 368 | + | ||||
| 369 | + | // Write a legacy value straight into the database. | |||
| 370 | + | db, err := kv.Open(ctx, dir, kv.Options{}) | |||
| 371 | + | require.NoError(t, err) | |||
| 372 | + | legacy, err := proto.Marshal(&storagepb.Repo{Name: "legacy", Visibility: storagepb.Visibility_VISIBILITY_PUBLIC}) | |||
| 373 | + | require.NoError(t, err) | |||
| 374 | + | var b kv.Batch | |||
| 375 | + | b.Put([]byte("repo/legacy"), legacy) | |||
| 376 | + | _, err = db.Apply(ctx, &b) | |||
| 377 | + | require.NoError(t, err) | |||
| 378 | + | require.NoError(t, db.Close(ctx)) | |||
| 379 | + | ||||
| 380 | + | s = openStore(t, dir) | |||
| 381 | + | got, err := s.Get(ctx, name(t, "legacy")) | |||
| 382 | + | require.NoError(t, err) | |||
| 383 | + | assert.Equal(t, domain.DefaultRepoSettings(), got.Settings) | |||
| 384 | + | list, err := s.List(ctx) | |||
| 385 | + | require.NoError(t, err) | |||
| 386 | + | require.Len(t, list, 1) | |||
| 387 | + | assert.Equal(t, domain.DefaultRepoSettings(), list[0].Settings) | |||
| 388 | + | } | |||
| 389 | + | ||||
| 390 | + | func TestSigningSeed(t *testing.T) { | |||
| 391 | + | ctx := context.Background() | |||
| 392 | + | dir := t.TempDir() | |||
| 393 | + | s := openStore(t, dir) | |||
| 394 | + | calls := 0 | |||
| 395 | + | gen := func() ([]byte, error) { | |||
| 396 | + | calls++ | |||
| 397 | + | return []byte(strings.Repeat(string(rune('a'+calls)), kvstore.SigningSeedLen)), nil | |||
| 398 | + | } | |||
| 399 | + | ||||
| 400 | + | first, err := s.SigningSeed(ctx, gen, created) | |||
| 401 | + | require.NoError(t, err) | |||
| 402 | + | require.Len(t, first, kvstore.SigningSeedLen) | |||
| 403 | + | second, err := s.SigningSeed(ctx, gen, created) | |||
| 404 | + | require.NoError(t, err) | |||
| 405 | + | assert.Equal(t, first, second, "generated once") | |||
| 406 | + | assert.Equal(t, 1, calls) | |||
| 407 | + | ||||
| 408 | + | // It survives a restart. | |||
| 409 | + | require.NoError(t, s.Close(ctx)) | |||
| 410 | + | s2 := openStore(t, dir) | |||
| 411 | + | third, err := s2.SigningSeed(ctx, gen, created) | |||
| 412 | + | require.NoError(t, err) | |||
| 413 | + | assert.Equal(t, first, third) | |||
| 414 | + | assert.Equal(t, 1, calls) | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | func TestSigningSeedConcurrentFirstStart(t *testing.T) { | |||
| 418 | + | ctx := context.Background() | |||
| 419 | + | s := openStore(t, t.TempDir()) | |||
| 420 | + | var ( | |||
| 421 | + | wg sync.WaitGroup | |||
| 422 | + | mu sync.Mutex | |||
| 423 | + | seeds [][]byte | |||
| 424 | + | n byte | |||
| 425 | + | ) | |||
| 426 | + | for range 6 { | |||
| 427 | + | wg.Go(func() { | |||
| 428 | + | seed, err := s.SigningSeed(ctx, func() ([]byte, error) { | |||
| 429 | + | mu.Lock() | |||
| 430 | + | defer mu.Unlock() | |||
| 431 | + | n++ | |||
| 432 | + | return []byte(strings.Repeat(string(rune('A'+n)), kvstore.SigningSeedLen)), nil | |||
| 433 | + | }, created) | |||
| 434 | + | if assert.NoError(t, err) { | |||
| 435 | + | mu.Lock() | |||
| 436 | + | seeds = append(seeds, seed) | |||
| 437 | + | mu.Unlock() | |||
| 438 | + | } | |||
| 439 | + | }) | |||
| 440 | + | } | |||
| 441 | + | wg.Wait() | |||
| 442 | + | for _, seed := range seeds { | |||
| 443 | + | assert.Equal(t, seeds[0], seed, "every caller gets the winner's seed") | |||
| 444 | + | } | |||
| 445 | + | } | |||
| 446 | + | ||||
| 447 | + | func TestSigningSeedRejectsBadGenerators(t *testing.T) { | |||
| 448 | + | ctx := context.Background() | |||
| 449 | + | s := openStore(t, t.TempDir()) | |||
| 450 | + | _, err := s.SigningSeed(ctx, func() ([]byte, error) { return []byte("short"), nil }, created) | |||
| 451 | + | require.Error(t, err) | |||
| 452 | + | assert.NotContains(t, err.Error(), "short") | |||
| 453 | + | _, err = s.SigningSeed(ctx, func() ([]byte, error) { return nil, assert.AnError }, created) | |||
| 454 | + | require.ErrorIs(t, err, assert.AnError) | |||
| 455 | + | } | |||
M internal/adapters/kvstore/repos.go +40 -1
| @@ -90,9 +90,36 @@ func (s *Store) Update(ctx context.Context, repo domain.Repo) error { | |||
| 90 | 90 | return nil | |
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | - | // Delete removes the repository; a missing one is not an error. | |
| 93 | + | // deleteChunk is how many merge request keys one batch deletes before the | |
| 94 | + | // last one, which also removes the repository. | |
| 95 | + | const deleteChunk = 1000 | |
| 96 | + | ||
| 97 | + | // Delete removes the repository and everything keyed to it: its merge | |
| 98 | + | // requests, their comments and read positions, and its number sequence. The | |
| 99 | + | // Git data is moved to the trash by the caller; the merge requests are not | |
| 100 | + | // kept anywhere, like the repository metadata, so a repository created again | |
| 101 | + | // under the same name starts clean, with its numbers starting at 1. A missing | |
| 102 | + | // repository is not an error. | |
| 103 | + | // | |
| 104 | + | // The merge request keys go first, in chunks, and the repository key goes with | |
| 105 | + | // the last chunk: a delete that fails half way leaves the repository, which | |
| 106 | + | // the caller can delete again. | |
| 94 | 107 | func (s *Store) Delete(ctx context.Context, name domain.RepoName) error { | |
| 108 | + | keys := s.mergeRequestKeys(ctx, name) | |
| 109 | + | for len(keys) > deleteChunk { | |
| 110 | + | var b kv.Batch | |
| 111 | + | for _, k := range keys[:deleteChunk] { | |
| 112 | + | b.Delete(k) | |
| 113 | + | } | |
| 114 | + | if _, err := s.db.Apply(ctx, &b); err != nil { | |
| 115 | + | return fmt.Errorf("delete merge requests of %q: %w", name, err) | |
| 116 | + | } | |
| 117 | + | keys = keys[deleteChunk:] | |
| 118 | + | } | |
| 95 | 119 | var b kv.Batch | |
| 120 | + | for _, k := range keys { | |
| 121 | + | b.Delete(k) | |
| 122 | + | } | |
| 96 | 123 | b.Delete(repoKey(name)) | |
| 97 | 124 | if _, err := s.db.Apply(ctx, &b); err != nil { | |
| 98 | 125 | return fmt.Errorf("delete repository %q: %w", name, err) | |
| @@ -111,12 +138,23 @@ func encodeRepo(r domain.Repo) ([]byte, error) { | |||
| 111 | 138 | if err != nil { | |
| 112 | 139 | return nil, err | |
| 113 | 140 | } | |
| 141 | + | settings := r.Settings | |
| 142 | + | if settings.IsZero() { | |
| 143 | + | // Settings are always stored explicitly, so that a later change of | |
| 144 | + | // the defaults never alters a stored repository. | |
| 145 | + | settings = domain.DefaultRepoSettings() | |
| 146 | + | } | |
| 147 | + | pbSettings, err := encodeSettings(settings) | |
| 148 | + | if err != nil { | |
| 149 | + | return nil, err | |
| 150 | + | } | |
| 114 | 151 | pb := &storagepb.Repo{ | |
| 115 | 152 | Name: r.Name.String(), | |
| 116 | 153 | Description: r.Description, | |
| 117 | 154 | Visibility: vis, | |
| 118 | 155 | CreatedAt: encodeTime(r.CreatedAt), | |
| 119 | 156 | PushedAt: encodeTime(r.PushedAt), | |
| 157 | + | Settings: pbSettings, | |
| 120 | 158 | } | |
| 121 | 159 | return mustMarshal(pb), nil | |
| 122 | 160 | } | |
| @@ -136,6 +174,7 @@ func decodeRepo(val []byte) (domain.Repo, error) { | |||
| 136 | 174 | Visibility: decodeVisibility(pb.GetVisibility()), | |
| 137 | 175 | CreatedAt: decodeTime(pb.GetCreatedAt()), | |
| 138 | 176 | PushedAt: decodeTime(pb.GetPushedAt()), | |
| 177 | + | Settings: decodeSettings(pb.GetSettings()), | |
| 139 | 178 | }, nil | |
| 140 | 179 | } | |
| 141 | 180 | ||
| @@ -90,9 +90,36 @@ func (s *Store) Update(ctx context.Context, repo domain.Repo) error { | |||||
| 90 | return nil | 90 | return nil | ||
| 91 | } | 91 | } | ||
| 92 | 92 | ||||
| 93 | - | // Delete removes the repository; a missing one is not an error. | 93 | + | // deleteChunk is how many merge request keys one batch deletes before the |
| 94 | + | // last one, which also removes the repository. | |||
| 95 | + | const deleteChunk = 1000 | |||
| 96 | + | ||||
| 97 | + | // Delete removes the repository and everything keyed to it: its merge | |||
| 98 | + | // requests, their comments and read positions, and its number sequence. The | |||
| 99 | + | // Git data is moved to the trash by the caller; the merge requests are not | |||
| 100 | + | // kept anywhere, like the repository metadata, so a repository created again | |||
| 101 | + | // under the same name starts clean, with its numbers starting at 1. A missing | |||
| 102 | + | // repository is not an error. | |||
| 103 | + | // | |||
| 104 | + | // The merge request keys go first, in chunks, and the repository key goes with | |||
| 105 | + | // the last chunk: a delete that fails half way leaves the repository, which | |||
| 106 | + | // the caller can delete again. | |||
| 94 | func (s *Store) Delete(ctx context.Context, name domain.RepoName) error { | 107 | func (s *Store) Delete(ctx context.Context, name domain.RepoName) error { | ||
| 108 | + | keys := s.mergeRequestKeys(ctx, name) | |||
| 109 | + | for len(keys) > deleteChunk { | |||
| 110 | + | var b kv.Batch | |||
| 111 | + | for _, k := range keys[:deleteChunk] { | |||
| 112 | + | b.Delete(k) | |||
| 113 | + | } | |||
| 114 | + | if _, err := s.db.Apply(ctx, &b); err != nil { | |||
| 115 | + | return fmt.Errorf("delete merge requests of %q: %w", name, err) | |||
| 116 | + | } | |||
| 117 | + | keys = keys[deleteChunk:] | |||
| 118 | + | } | |||
| 95 | var b kv.Batch | 119 | var b kv.Batch | ||
| 120 | + | for _, k := range keys { | |||
| 121 | + | b.Delete(k) | |||
| 122 | + | } | |||
| 96 | b.Delete(repoKey(name)) | 123 | b.Delete(repoKey(name)) | ||
| 97 | if _, err := s.db.Apply(ctx, &b); err != nil { | 124 | if _, err := s.db.Apply(ctx, &b); err != nil { | ||
| 98 | return fmt.Errorf("delete repository %q: %w", name, err) | 125 | return fmt.Errorf("delete repository %q: %w", name, err) | ||
| @@ -111,12 +138,23 @@ func encodeRepo(r domain.Repo) ([]byte, error) { | |||||
| 111 | if err != nil { | 138 | if err != nil { | ||
| 112 | return nil, err | 139 | return nil, err | ||
| 113 | } | 140 | } | ||
| 141 | + | settings := r.Settings | |||
| 142 | + | if settings.IsZero() { | |||
| 143 | + | // Settings are always stored explicitly, so that a later change of | |||
| 144 | + | // the defaults never alters a stored repository. | |||
| 145 | + | settings = domain.DefaultRepoSettings() | |||
| 146 | + | } | |||
| 147 | + | pbSettings, err := encodeSettings(settings) | |||
| 148 | + | if err != nil { | |||
| 149 | + | return nil, err | |||
| 150 | + | } | |||
| 114 | pb := &storagepb.Repo{ | 151 | pb := &storagepb.Repo{ | ||
| 115 | Name: r.Name.String(), | 152 | Name: r.Name.String(), | ||
| 116 | Description: r.Description, | 153 | Description: r.Description, | ||
| 117 | Visibility: vis, | 154 | Visibility: vis, | ||
| 118 | CreatedAt: encodeTime(r.CreatedAt), | 155 | CreatedAt: encodeTime(r.CreatedAt), | ||
| 119 | PushedAt: encodeTime(r.PushedAt), | 156 | PushedAt: encodeTime(r.PushedAt), | ||
| 157 | + | Settings: pbSettings, | |||
| 120 | } | 158 | } | ||
| 121 | return mustMarshal(pb), nil | 159 | return mustMarshal(pb), nil | ||
| 122 | } | 160 | } | ||
| @@ -136,6 +174,7 @@ func decodeRepo(val []byte) (domain.Repo, error) { | |||||
| 136 | Visibility: decodeVisibility(pb.GetVisibility()), | 174 | Visibility: decodeVisibility(pb.GetVisibility()), | ||
| 137 | CreatedAt: decodeTime(pb.GetCreatedAt()), | 175 | CreatedAt: decodeTime(pb.GetCreatedAt()), | ||
| 138 | PushedAt: decodeTime(pb.GetPushedAt()), | 176 | PushedAt: decodeTime(pb.GetPushedAt()), | ||
| 177 | + | Settings: decodeSettings(pb.GetSettings()), | |||
| 139 | }, nil | 178 | }, nil | ||
| 140 | } | 179 | } | ||
| 141 | 180 | ||||
A internal/adapters/kvstore/signing.go +63 -0
| @@ -0,0 +1,63 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package kvstore | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "errors" | |
| 8 | + | "fmt" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | kv "bnema.dev/ermite-kv" | |
| 12 | + | "google.golang.org/protobuf/proto" | |
| 13 | + | ||
| 14 | + | storagepb "bnema.dev/ermite/gen/ermite/storage" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | const signingKeyKey = "signing/key" | |
| 18 | + | ||
| 19 | + | // SigningSeedLen is the length of the stored seed of the signing key. | |
| 20 | + | const SigningSeedLen = 32 | |
| 21 | + | ||
| 22 | + | // SigningSeed returns the 32-byte seed of the commit signing key (ADR 0022). | |
| 23 | + | // When none is stored, it calls generate, stores what it returns and returns | |
| 24 | + | // that; of two concurrent first calls one wins and both return its seed. The | |
| 25 | + | // seed is the private key: neither it nor an error carrying it is logged. | |
| 26 | + | func (s *Store) SigningSeed(ctx context.Context, generate func() ([]byte, error), now time.Time) ([]byte, error) { | |
| 27 | + | key := []byte(signingKeyKey) | |
| 28 | + | for range 2 { | |
| 29 | + | if item, ok := s.db.Get(ctx, key); ok { | |
| 30 | + | return decodeSigningSeed(item.Value) | |
| 31 | + | } | |
| 32 | + | seed, err := generate() | |
| 33 | + | if err != nil { | |
| 34 | + | return nil, fmt.Errorf("generate the signing key: %w", err) | |
| 35 | + | } | |
| 36 | + | if len(seed) != SigningSeedLen { | |
| 37 | + | return nil, fmt.Errorf("generate the signing key: seed has %d bytes, want %d", len(seed), SigningSeedLen) | |
| 38 | + | } | |
| 39 | + | var b kv.Batch | |
| 40 | + | b.RequireAbsent(key) | |
| 41 | + | b.Put(key, mustMarshal(&storagepb.SigningKey{Seed: seed, CreatedAt: encodeTime(now)})) | |
| 42 | + | _, err = s.db.Apply(ctx, &b) | |
| 43 | + | if errors.Is(err, kv.ErrConflict) { | |
| 44 | + | continue // written meanwhile: read it | |
| 45 | + | } | |
| 46 | + | if err != nil { | |
| 47 | + | return nil, fmt.Errorf("store the signing key: %w", err) | |
| 48 | + | } | |
| 49 | + | return seed, nil | |
| 50 | + | } | |
| 51 | + | return nil, errors.New("store the signing key: concurrent initialization did not settle") | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | func decodeSigningSeed(val []byte) ([]byte, error) { | |
| 55 | + | var pb storagepb.SigningKey | |
| 56 | + | if err := proto.Unmarshal(val, &pb); err != nil { | |
| 57 | + | return nil, fmt.Errorf("decode the signing key: %w", err) | |
| 58 | + | } | |
| 59 | + | if len(pb.GetSeed()) != SigningSeedLen { | |
| 60 | + | return nil, fmt.Errorf("decode the signing key: seed has %d bytes, want %d", len(pb.GetSeed()), SigningSeedLen) | |
| 61 | + | } | |
| 62 | + | return pb.GetSeed(), nil | |
| 63 | + | } | |
| @@ -0,0 +1,63 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package kvstore | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "errors" | |||
| 8 | + | "fmt" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | kv "bnema.dev/ermite-kv" | |||
| 12 | + | "google.golang.org/protobuf/proto" | |||
| 13 | + | ||||
| 14 | + | storagepb "bnema.dev/ermite/gen/ermite/storage" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | const signingKeyKey = "signing/key" | |||
| 18 | + | ||||
| 19 | + | // SigningSeedLen is the length of the stored seed of the signing key. | |||
| 20 | + | const SigningSeedLen = 32 | |||
| 21 | + | ||||
| 22 | + | // SigningSeed returns the 32-byte seed of the commit signing key (ADR 0022). | |||
| 23 | + | // When none is stored, it calls generate, stores what it returns and returns | |||
| 24 | + | // that; of two concurrent first calls one wins and both return its seed. The | |||
| 25 | + | // seed is the private key: neither it nor an error carrying it is logged. | |||
| 26 | + | func (s *Store) SigningSeed(ctx context.Context, generate func() ([]byte, error), now time.Time) ([]byte, error) { | |||
| 27 | + | key := []byte(signingKeyKey) | |||
| 28 | + | for range 2 { | |||
| 29 | + | if item, ok := s.db.Get(ctx, key); ok { | |||
| 30 | + | return decodeSigningSeed(item.Value) | |||
| 31 | + | } | |||
| 32 | + | seed, err := generate() | |||
| 33 | + | if err != nil { | |||
| 34 | + | return nil, fmt.Errorf("generate the signing key: %w", err) | |||
| 35 | + | } | |||
| 36 | + | if len(seed) != SigningSeedLen { | |||
| 37 | + | return nil, fmt.Errorf("generate the signing key: seed has %d bytes, want %d", len(seed), SigningSeedLen) | |||
| 38 | + | } | |||
| 39 | + | var b kv.Batch | |||
| 40 | + | b.RequireAbsent(key) | |||
| 41 | + | b.Put(key, mustMarshal(&storagepb.SigningKey{Seed: seed, CreatedAt: encodeTime(now)})) | |||
| 42 | + | _, err = s.db.Apply(ctx, &b) | |||
| 43 | + | if errors.Is(err, kv.ErrConflict) { | |||
| 44 | + | continue // written meanwhile: read it | |||
| 45 | + | } | |||
| 46 | + | if err != nil { | |||
| 47 | + | return nil, fmt.Errorf("store the signing key: %w", err) | |||
| 48 | + | } | |||
| 49 | + | return seed, nil | |||
| 50 | + | } | |||
| 51 | + | return nil, errors.New("store the signing key: concurrent initialization did not settle") | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | func decodeSigningSeed(val []byte) ([]byte, error) { | |||
| 55 | + | var pb storagepb.SigningKey | |||
| 56 | + | if err := proto.Unmarshal(val, &pb); err != nil { | |||
| 57 | + | return nil, fmt.Errorf("decode the signing key: %w", err) | |||
| 58 | + | } | |||
| 59 | + | if len(pb.GetSeed()) != SigningSeedLen { | |||
| 60 | + | return nil, fmt.Errorf("decode the signing key: seed has %d bytes, want %d", len(pb.GetSeed()), SigningSeedLen) | |||
| 61 | + | } | |||
| 62 | + | return pb.GetSeed(), nil | |||
| 63 | + | } | |||
M internal/adapters/kvstore/store_test.go +3 -3
| @@ -42,8 +42,8 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||
| 42 | 42 | ctx := context.Background() | |
| 43 | 43 | s := openStore(t, t.TempDir()) | |
| 44 | 44 | ||
| 45 | - | b := domain.Repo{Name: name(t, "b"), Description: "second", Visibility: domain.VisibilityPublic, CreatedAt: created} | |
| 46 | - | a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour)} | |
| 45 | + | b := domain.Repo{Name: name(t, "b"), Description: "second", Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()} | |
| 46 | + | a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), Settings: domain.DefaultRepoSettings()} | |
| 47 | 47 | require.NoError(t, s.Create(ctx, b)) | |
| 48 | 48 | require.NoError(t, s.Create(ctx, a)) | |
| 49 | 49 | ||
| @@ -129,7 +129,7 @@ func TestPersistsAcrossReopen(t *testing.T) { | |||
| 129 | 129 | dir := t.TempDir() | |
| 130 | 130 | s, err := kvstore.Open(ctx, dir) | |
| 131 | 131 | require.NoError(t, err) | |
| 132 | - | r := domain.Repo{Name: name(t, "keep"), Visibility: domain.VisibilityPublic, CreatedAt: created} | |
| 132 | + | r := domain.Repo{Name: name(t, "keep"), Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()} | |
| 133 | 133 | require.NoError(t, s.Create(ctx, r)) | |
| 134 | 134 | require.NoError(t, s.Close(ctx)) | |
| 135 | 135 | ||
| @@ -42,8 +42,8 @@ func TestCreateGetListUpdateDelete(t *testing.T) { | |||||
| 42 | ctx := context.Background() | 42 | ctx := context.Background() | ||
| 43 | s := openStore(t, t.TempDir()) | 43 | s := openStore(t, t.TempDir()) | ||
| 44 | 44 | ||||
| 45 | - | b := domain.Repo{Name: name(t, "b"), Description: "second", Visibility: domain.VisibilityPublic, CreatedAt: created} | 45 | + | b := domain.Repo{Name: name(t, "b"), Description: "second", Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()} |
| 46 | - | a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour)} | 46 | + | a := domain.Repo{Name: name(t, "a"), Visibility: domain.VisibilityPrivate, CreatedAt: created, PushedAt: created.Add(time.Hour), Settings: domain.DefaultRepoSettings()} |
| 47 | require.NoError(t, s.Create(ctx, b)) | 47 | require.NoError(t, s.Create(ctx, b)) | ||
| 48 | require.NoError(t, s.Create(ctx, a)) | 48 | require.NoError(t, s.Create(ctx, a)) | ||
| 49 | 49 | ||||
| @@ -129,7 +129,7 @@ func TestPersistsAcrossReopen(t *testing.T) { | |||||
| 129 | dir := t.TempDir() | 129 | dir := t.TempDir() | ||
| 130 | s, err := kvstore.Open(ctx, dir) | 130 | s, err := kvstore.Open(ctx, dir) | ||
| 131 | require.NoError(t, err) | 131 | require.NoError(t, err) | ||
| 132 | - | r := domain.Repo{Name: name(t, "keep"), Visibility: domain.VisibilityPublic, CreatedAt: created} | 132 | + | r := domain.Repo{Name: name(t, "keep"), Visibility: domain.VisibilityPublic, CreatedAt: created, Settings: domain.DefaultRepoSettings()} |
| 133 | require.NoError(t, s.Create(ctx, r)) | 133 | require.NoError(t, s.Create(ctx, r)) | ||
| 134 | require.NoError(t, s.Close(ctx)) | 134 | require.NoError(t, s.Close(ctx)) | ||
| 135 | 135 | ||||
A proto/ermite/storage/merge_request.proto +84 -0
| @@ -0,0 +1,84 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | // Internal, unversioned storage schema of ermite: merge requests. | |
| 4 | + | ||
| 5 | + | syntax = "proto3"; | |
| 6 | + | ||
| 7 | + | package ermite.storage; | |
| 8 | + | ||
| 9 | + | import "google/protobuf/timestamp.proto"; | |
| 10 | + | ||
| 11 | + | option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb"; | |
| 12 | + | ||
| 13 | + | // MergeMode is how a merge request is merged. | |
| 14 | + | enum MergeMode { | |
| 15 | + | MERGE_MODE_UNSPECIFIED = 0; | |
| 16 | + | MERGE_MODE_FAST_FORWARD = 1; | |
| 17 | + | MERGE_MODE_SQUASH = 2; | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | // RepoSettings are the merge settings of a repository, stored inside Repo. | |
| 21 | + | message RepoSettings { | |
| 22 | + | // Never empty once stored. | |
| 23 | + | repeated MergeMode allowed_merge_modes = 1; | |
| 24 | + | bool failing_checks_block = 2; | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // MergeRequestState is where a merge request is in its life. | |
| 28 | + | enum MergeRequestState { | |
| 29 | + | MERGE_REQUEST_STATE_UNSPECIFIED = 0; | |
| 30 | + | MERGE_REQUEST_STATE_OPEN = 1; | |
| 31 | + | MERGE_REQUEST_STATE_MERGED = 2; | |
| 32 | + | MERGE_REQUEST_STATE_CLOSED = 3; | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // DiffSide is the side of a diff a comment is attached to. | |
| 36 | + | enum DiffSide { | |
| 37 | + | // A general comment. | |
| 38 | + | DIFF_SIDE_UNSPECIFIED = 0; | |
| 39 | + | DIFF_SIDE_OLD = 1; | |
| 40 | + | DIFF_SIDE_NEW = 2; | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // MergeRequest is stored under the key "mr/<repo>/<number:010d>". | |
| 44 | + | message MergeRequest { | |
| 45 | + | int64 number = 1; | |
| 46 | + | string title = 2; | |
| 47 | + | string description = 3; | |
| 48 | + | string source_branch = 4; | |
| 49 | + | string target_branch = 5; | |
| 50 | + | MergeRequestState state = 6; | |
| 51 | + | // Label of the token that opened it. | |
| 52 | + | string author = 7; | |
| 53 | + | google.protobuf.Timestamp created_at = 8; | |
| 54 | + | google.protobuf.Timestamp updated_at = 9; | |
| 55 | + | // Head of the source branch when the request was merged or closed; empty | |
| 56 | + | // while it is open (the head is read from Git then). | |
| 57 | + | string head_id = 10; | |
| 58 | + | // Merge base of the target and the head when the request was merged or | |
| 59 | + | // closed, so that its diff stays readable after the merge. | |
| 60 | + | string base_id = 11; | |
| 61 | + | string merged_commit_id = 12; | |
| 62 | + | MergeMode merge_mode = 13; | |
| 63 | + | // Last comment id handed out for this request. | |
| 64 | + | int64 comment_seq = 14; | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // Comment is stored under the key "mrc/<repo>/<number:010d>/<id:010d>". | |
| 68 | + | message Comment { | |
| 69 | + | int64 id = 1; | |
| 70 | + | string author = 2; | |
| 71 | + | string path = 3; | |
| 72 | + | int32 line = 4; | |
| 73 | + | DiffSide side = 5; | |
| 74 | + | string commit_id = 6; | |
| 75 | + | string text = 7; | |
| 76 | + | bool resolved = 8; | |
| 77 | + | google.protobuf.Timestamp created_at = 9; | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | // Sequence is stored under the key "mrseq/<repo>": the last merge request | |
| 81 | + | // number handed out for the repository. | |
| 82 | + | message Sequence { | |
| 83 | + | int64 last = 1; | |
| 84 | + | } | |
| @@ -0,0 +1,84 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | // Internal, unversioned storage schema of ermite: merge requests. | |||
| 4 | + | ||||
| 5 | + | syntax = "proto3"; | |||
| 6 | + | ||||
| 7 | + | package ermite.storage; | |||
| 8 | + | ||||
| 9 | + | import "google/protobuf/timestamp.proto"; | |||
| 10 | + | ||||
| 11 | + | option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb"; | |||
| 12 | + | ||||
| 13 | + | // MergeMode is how a merge request is merged. | |||
| 14 | + | enum MergeMode { | |||
| 15 | + | MERGE_MODE_UNSPECIFIED = 0; | |||
| 16 | + | MERGE_MODE_FAST_FORWARD = 1; | |||
| 17 | + | MERGE_MODE_SQUASH = 2; | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | // RepoSettings are the merge settings of a repository, stored inside Repo. | |||
| 21 | + | message RepoSettings { | |||
| 22 | + | // Never empty once stored. | |||
| 23 | + | repeated MergeMode allowed_merge_modes = 1; | |||
| 24 | + | bool failing_checks_block = 2; | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // MergeRequestState is where a merge request is in its life. | |||
| 28 | + | enum MergeRequestState { | |||
| 29 | + | MERGE_REQUEST_STATE_UNSPECIFIED = 0; | |||
| 30 | + | MERGE_REQUEST_STATE_OPEN = 1; | |||
| 31 | + | MERGE_REQUEST_STATE_MERGED = 2; | |||
| 32 | + | MERGE_REQUEST_STATE_CLOSED = 3; | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // DiffSide is the side of a diff a comment is attached to. | |||
| 36 | + | enum DiffSide { | |||
| 37 | + | // A general comment. | |||
| 38 | + | DIFF_SIDE_UNSPECIFIED = 0; | |||
| 39 | + | DIFF_SIDE_OLD = 1; | |||
| 40 | + | DIFF_SIDE_NEW = 2; | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // MergeRequest is stored under the key "mr/<repo>/<number:010d>". | |||
| 44 | + | message MergeRequest { | |||
| 45 | + | int64 number = 1; | |||
| 46 | + | string title = 2; | |||
| 47 | + | string description = 3; | |||
| 48 | + | string source_branch = 4; | |||
| 49 | + | string target_branch = 5; | |||
| 50 | + | MergeRequestState state = 6; | |||
| 51 | + | // Label of the token that opened it. | |||
| 52 | + | string author = 7; | |||
| 53 | + | google.protobuf.Timestamp created_at = 8; | |||
| 54 | + | google.protobuf.Timestamp updated_at = 9; | |||
| 55 | + | // Head of the source branch when the request was merged or closed; empty | |||
| 56 | + | // while it is open (the head is read from Git then). | |||
| 57 | + | string head_id = 10; | |||
| 58 | + | // Merge base of the target and the head when the request was merged or | |||
| 59 | + | // closed, so that its diff stays readable after the merge. | |||
| 60 | + | string base_id = 11; | |||
| 61 | + | string merged_commit_id = 12; | |||
| 62 | + | MergeMode merge_mode = 13; | |||
| 63 | + | // Last comment id handed out for this request. | |||
| 64 | + | int64 comment_seq = 14; | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // Comment is stored under the key "mrc/<repo>/<number:010d>/<id:010d>". | |||
| 68 | + | message Comment { | |||
| 69 | + | int64 id = 1; | |||
| 70 | + | string author = 2; | |||
| 71 | + | string path = 3; | |||
| 72 | + | int32 line = 4; | |||
| 73 | + | DiffSide side = 5; | |||
| 74 | + | string commit_id = 6; | |||
| 75 | + | string text = 7; | |||
| 76 | + | bool resolved = 8; | |||
| 77 | + | google.protobuf.Timestamp created_at = 9; | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | // Sequence is stored under the key "mrseq/<repo>": the last merge request | |||
| 81 | + | // number handed out for the repository. | |||
| 82 | + | message Sequence { | |||
| 83 | + | int64 last = 1; | |||
| 84 | + | } | |||
M proto/ermite/storage/repo.proto +4 -0
| @@ -6,6 +6,7 @@ syntax = "proto3"; | |||
| 6 | 6 | ||
| 7 | 7 | package ermite.storage; | |
| 8 | 8 | ||
| 9 | + | import "ermite/storage/merge_request.proto"; | |
| 9 | 10 | import "google/protobuf/timestamp.proto"; | |
| 10 | 11 | ||
| 11 | 12 | option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb"; | |
| @@ -26,6 +27,9 @@ message Repo { | |||
| 26 | 27 | google.protobuf.Timestamp created_at = 4; | |
| 27 | 28 | // Unset until the first push. | |
| 28 | 29 | google.protobuf.Timestamp pushed_at = 5; | |
| 30 | + | // Unset on repositories stored before merge requests: they read as the | |
| 31 | + | // default settings. | |
| 32 | + | RepoSettings settings = 6; | |
| 29 | 33 | } | |
| 30 | 34 | ||
| 31 | 35 | // SchemaVersion is stored under the key "meta/schema". | |
| @@ -6,6 +6,7 @@ syntax = "proto3"; | |||||
| 6 | 6 | ||||
| 7 | package ermite.storage; | 7 | package ermite.storage; | ||
| 8 | 8 | ||||
| 9 | + | import "ermite/storage/merge_request.proto"; | |||
| 9 | import "google/protobuf/timestamp.proto"; | 10 | import "google/protobuf/timestamp.proto"; | ||
| 10 | 11 | ||||
| 11 | option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb"; | 12 | option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb"; | ||
| @@ -26,6 +27,9 @@ message Repo { | |||||
| 26 | google.protobuf.Timestamp created_at = 4; | 27 | google.protobuf.Timestamp created_at = 4; | ||
| 27 | // Unset until the first push. | 28 | // Unset until the first push. | ||
| 28 | google.protobuf.Timestamp pushed_at = 5; | 29 | google.protobuf.Timestamp pushed_at = 5; | ||
| 30 | + | // Unset on repositories stored before merge requests: they read as the | |||
| 31 | + | // default settings. | |||
| 32 | + | RepoSettings settings = 6; | |||
| 29 | } | 33 | } | ||
| 30 | 34 | ||||
| 31 | // SchemaVersion is stored under the key "meta/schema". | 35 | // SchemaVersion is stored under the key "meta/schema". | ||
A proto/ermite/storage/signing.proto +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | // Internal, unversioned storage schema of ermite: the commit signing key. | |
| 4 | + | ||
| 5 | + | syntax = "proto3"; | |
| 6 | + | ||
| 7 | + | package ermite.storage; | |
| 8 | + | ||
| 9 | + | import "google/protobuf/timestamp.proto"; | |
| 10 | + | ||
| 11 | + | option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb"; | |
| 12 | + | ||
| 13 | + | // SigningKey is stored under the key "signing/key". The seed is the private | |
| 14 | + | // key: it must never be logged. | |
| 15 | + | message SigningKey { | |
| 16 | + | // The 32-byte seed of an ed25519 key. | |
| 17 | + | bytes seed = 1; | |
| 18 | + | google.protobuf.Timestamp created_at = 2; | |
| 19 | + | } | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | // Internal, unversioned storage schema of ermite: the commit signing key. | |||
| 4 | + | ||||
| 5 | + | syntax = "proto3"; | |||
| 6 | + | ||||
| 7 | + | package ermite.storage; | |||
| 8 | + | ||||
| 9 | + | import "google/protobuf/timestamp.proto"; | |||
| 10 | + | ||||
| 11 | + | option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb"; | |||
| 12 | + | ||||
| 13 | + | // SigningKey is stored under the key "signing/key". The seed is the private | |||
| 14 | + | // key: it must never be logged. | |||
| 15 | + | message SigningKey { | |||
| 16 | + | // The 32-byte seed of an ed25519 key. | |||
| 17 | + | bytes seed = 1; | |||
| 18 | + | google.protobuf.Timestamp created_at = 2; | |||
| 19 | + | } | |||