detached 3130756

feat(api): add issue service, issues repo setting and issue:write scope

commit
31307565591ef7f7a6d4ec73137f80611c88bd55
author
brice · · 2026-10-19 05:15 UTC
committer
brice · · 2026-10-19 08:28 UTC
parent
11411cf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +2205 -115
M gen/ermite/ermiteconnect/event.connect.go +4 -2
@@ -48,7 +48,8 @@ type EventServiceClient interface {
4848 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,
4949 // for merge requests, merge_request_opened, merge_request_merged,
5050 // merge_request_closed, comment_added, comment_edited, comment_deleted and
51- // comment_resolved (a thread resolved or reopened). Each one is delivered on the
51+ // comment_resolved (a thread resolved or reopened); for issues, issue_opened,
52+ // issue_closed, issue_reopened and issue_commented. Each one is delivered on the
5253 // "repo:<name>" topic of its repository and on "home", subject to the
5354 // visibility of the repository.
5455 //
@@ -121,7 +122,8 @@ type EventServiceHandler interface {
121122 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,
122123 // for merge requests, merge_request_opened, merge_request_merged,
123124 // merge_request_closed, comment_added, comment_edited, comment_deleted and
124- // comment_resolved (a thread resolved or reopened). Each one is delivered on the
125+ // comment_resolved (a thread resolved or reopened); for issues, issue_opened,
126+ // issue_closed, issue_reopened and issue_commented. Each one is delivered on the
125127 // "repo:<name>" topic of its repository and on "home", subject to the
126128 // visibility of the repository.
127129 //
@@ -48,7 +48,8 @@ type EventServiceClient interface {
48 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,48 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,
49 // for merge requests, merge_request_opened, merge_request_merged,49 // for merge requests, merge_request_opened, merge_request_merged,
50 // merge_request_closed, comment_added, comment_edited, comment_deleted and50 // merge_request_closed, comment_added, comment_edited, comment_deleted and
51- // comment_resolved (a thread resolved or reopened). Each one is delivered on the51+ // comment_resolved (a thread resolved or reopened); for issues, issue_opened,
52+ // issue_closed, issue_reopened and issue_commented. Each one is delivered on the
52 // "repo:<name>" topic of its repository and on "home", subject to the53 // "repo:<name>" topic of its repository and on "home", subject to the
53 // visibility of the repository.54 // visibility of the repository.
54 //55 //
@@ -121,7 +122,8 @@ type EventServiceHandler interface {
121 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,122 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,
122 // for merge requests, merge_request_opened, merge_request_merged,123 // for merge requests, merge_request_opened, merge_request_merged,
123 // merge_request_closed, comment_added, comment_edited, comment_deleted and124 // merge_request_closed, comment_added, comment_edited, comment_deleted and
124- // comment_resolved (a thread resolved or reopened). Each one is delivered on the125+ // comment_resolved (a thread resolved or reopened); for issues, issue_opened,
126+ // issue_closed, issue_reopened and issue_commented. Each one is delivered on the
125 // "repo:<name>" topic of its repository and on "home", subject to the127 // "repo:<name>" topic of its repository and on "home", subject to the
126 // visibility of the repository.128 // visibility of the repository.
127 //129 //
A gen/ermite/ermiteconnect/issue.connect.go +265 -0
@@ -0,0 +1,265 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
4+//
5+// Source: ermite/issue.proto
6+
7+package ermiteconnect
8+
9+import (
10+ ermite "bnema.dev/ermite-api/gen/ermite"
11+ connect "connectrpc.com/connect"
12+ context "context"
13+ errors "errors"
14+ http "net/http"
15+ strings "strings"
16+)
17+
18+// This is a compile-time assertion to ensure that this generated file and the connect package are
19+// compatible. If you get a compiler error that this constant is not defined, this code was
20+// generated with a version of connect newer than the one compiled into your binary. You can fix the
21+// problem by either regenerating this code with an older version of connect or updating the connect
22+// version compiled into your binary.
23+const _ = connect.IsAtLeastVersion1_13_0
24+
25+const (
26+ // IssueServiceName is the fully-qualified name of the IssueService service.
27+ IssueServiceName = "ermite.IssueService"
28+)
29+
30+// These constants are the fully-qualified names of the RPCs defined in this package. They're
31+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
32+//
33+// Note that these are different from the fully-qualified method names used by
34+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
35+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
36+// period.
37+const (
38+ // IssueServiceCreateIssueProcedure is the fully-qualified name of the IssueService's CreateIssue
39+ // RPC.
40+ IssueServiceCreateIssueProcedure = "/ermite.IssueService/CreateIssue"
41+ // IssueServiceListIssuesProcedure is the fully-qualified name of the IssueService's ListIssues RPC.
42+ IssueServiceListIssuesProcedure = "/ermite.IssueService/ListIssues"
43+ // IssueServiceGetIssueProcedure is the fully-qualified name of the IssueService's GetIssue RPC.
44+ IssueServiceGetIssueProcedure = "/ermite.IssueService/GetIssue"
45+ // IssueServiceAddIssueCommentProcedure is the fully-qualified name of the IssueService's
46+ // AddIssueComment RPC.
47+ IssueServiceAddIssueCommentProcedure = "/ermite.IssueService/AddIssueComment"
48+ // IssueServiceCloseIssueProcedure is the fully-qualified name of the IssueService's CloseIssue RPC.
49+ IssueServiceCloseIssueProcedure = "/ermite.IssueService/CloseIssue"
50+ // IssueServiceReopenIssueProcedure is the fully-qualified name of the IssueService's ReopenIssue
51+ // RPC.
52+ IssueServiceReopenIssueProcedure = "/ermite.IssueService/ReopenIssue"
53+)
54+
55+// IssueServiceClient is a client for the ermite.IssueService service.
56+type IssueServiceClient interface {
57+ // CreateIssue opens an issue. Needs the issue:write scope.
58+ CreateIssue(context.Context, *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error)
59+ // ListIssues lists issues, highest number first, optionally in one state.
60+ ListIssues(context.Context, *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error)
61+ // GetIssue returns an issue with its comments.
62+ GetIssue(context.Context, *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error)
63+ // AddIssueComment adds a comment. Needs the issue:write scope.
64+ AddIssueComment(context.Context, *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error)
65+ // CloseIssue closes an issue. Needs the issue:write scope.
66+ CloseIssue(context.Context, *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error)
67+ // ReopenIssue reopens an issue. Needs the issue:write scope.
68+ ReopenIssue(context.Context, *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error)
69+}
70+
71+// NewIssueServiceClient constructs a client for the ermite.IssueService service. By default, it
72+// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
73+// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
74+// connect.WithGRPCWeb() options.
75+//
76+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
77+// http://api.acme.com or https://acme.com/grpc).
78+func NewIssueServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) IssueServiceClient {
79+ baseURL = strings.TrimRight(baseURL, "/")
80+ issueServiceMethods := ermite.File_ermite_issue_proto.Services().ByName("IssueService").Methods()
81+ return &issueServiceClient{
82+ createIssue: connect.NewClient[ermite.CreateIssueRequest, ermite.CreateIssueResponse](
83+ httpClient,
84+ baseURL+IssueServiceCreateIssueProcedure,
85+ connect.WithSchema(issueServiceMethods.ByName("CreateIssue")),
86+ connect.WithClientOptions(opts...),
87+ ),
88+ listIssues: connect.NewClient[ermite.ListIssuesRequest, ermite.ListIssuesResponse](
89+ httpClient,
90+ baseURL+IssueServiceListIssuesProcedure,
91+ connect.WithSchema(issueServiceMethods.ByName("ListIssues")),
92+ connect.WithClientOptions(opts...),
93+ ),
94+ getIssue: connect.NewClient[ermite.GetIssueRequest, ermite.GetIssueResponse](
95+ httpClient,
96+ baseURL+IssueServiceGetIssueProcedure,
97+ connect.WithSchema(issueServiceMethods.ByName("GetIssue")),
98+ connect.WithClientOptions(opts...),
99+ ),
100+ addIssueComment: connect.NewClient[ermite.AddIssueCommentRequest, ermite.AddIssueCommentResponse](
101+ httpClient,
102+ baseURL+IssueServiceAddIssueCommentProcedure,
103+ connect.WithSchema(issueServiceMethods.ByName("AddIssueComment")),
104+ connect.WithClientOptions(opts...),
105+ ),
106+ closeIssue: connect.NewClient[ermite.CloseIssueRequest, ermite.CloseIssueResponse](
107+ httpClient,
108+ baseURL+IssueServiceCloseIssueProcedure,
109+ connect.WithSchema(issueServiceMethods.ByName("CloseIssue")),
110+ connect.WithClientOptions(opts...),
111+ ),
112+ reopenIssue: connect.NewClient[ermite.ReopenIssueRequest, ermite.ReopenIssueResponse](
113+ httpClient,
114+ baseURL+IssueServiceReopenIssueProcedure,
115+ connect.WithSchema(issueServiceMethods.ByName("ReopenIssue")),
116+ connect.WithClientOptions(opts...),
117+ ),
118+ }
119+}
120+
121+// issueServiceClient implements IssueServiceClient.
122+type issueServiceClient struct {
123+ createIssue *connect.Client[ermite.CreateIssueRequest, ermite.CreateIssueResponse]
124+ listIssues *connect.Client[ermite.ListIssuesRequest, ermite.ListIssuesResponse]
125+ getIssue *connect.Client[ermite.GetIssueRequest, ermite.GetIssueResponse]
126+ addIssueComment *connect.Client[ermite.AddIssueCommentRequest, ermite.AddIssueCommentResponse]
127+ closeIssue *connect.Client[ermite.CloseIssueRequest, ermite.CloseIssueResponse]
128+ reopenIssue *connect.Client[ermite.ReopenIssueRequest, ermite.ReopenIssueResponse]
129+}
130+
131+// CreateIssue calls ermite.IssueService.CreateIssue.
132+func (c *issueServiceClient) CreateIssue(ctx context.Context, req *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error) {
133+ return c.createIssue.CallUnary(ctx, req)
134+}
135+
136+// ListIssues calls ermite.IssueService.ListIssues.
137+func (c *issueServiceClient) ListIssues(ctx context.Context, req *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error) {
138+ return c.listIssues.CallUnary(ctx, req)
139+}
140+
141+// GetIssue calls ermite.IssueService.GetIssue.
142+func (c *issueServiceClient) GetIssue(ctx context.Context, req *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error) {
143+ return c.getIssue.CallUnary(ctx, req)
144+}
145+
146+// AddIssueComment calls ermite.IssueService.AddIssueComment.
147+func (c *issueServiceClient) AddIssueComment(ctx context.Context, req *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error) {
148+ return c.addIssueComment.CallUnary(ctx, req)
149+}
150+
151+// CloseIssue calls ermite.IssueService.CloseIssue.
152+func (c *issueServiceClient) CloseIssue(ctx context.Context, req *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error) {
153+ return c.closeIssue.CallUnary(ctx, req)
154+}
155+
156+// ReopenIssue calls ermite.IssueService.ReopenIssue.
157+func (c *issueServiceClient) ReopenIssue(ctx context.Context, req *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error) {
158+ return c.reopenIssue.CallUnary(ctx, req)
159+}
160+
161+// IssueServiceHandler is an implementation of the ermite.IssueService service.
162+type IssueServiceHandler interface {
163+ // CreateIssue opens an issue. Needs the issue:write scope.
164+ CreateIssue(context.Context, *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error)
165+ // ListIssues lists issues, highest number first, optionally in one state.
166+ ListIssues(context.Context, *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error)
167+ // GetIssue returns an issue with its comments.
168+ GetIssue(context.Context, *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error)
169+ // AddIssueComment adds a comment. Needs the issue:write scope.
170+ AddIssueComment(context.Context, *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error)
171+ // CloseIssue closes an issue. Needs the issue:write scope.
172+ CloseIssue(context.Context, *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error)
173+ // ReopenIssue reopens an issue. Needs the issue:write scope.
174+ ReopenIssue(context.Context, *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error)
175+}
176+
177+// NewIssueServiceHandler builds an HTTP handler from the service implementation. It returns the
178+// path on which to mount the handler and the handler itself.
179+//
180+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
181+// and JSON codecs. They also support gzip compression.
182+func NewIssueServiceHandler(svc IssueServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
183+ issueServiceMethods := ermite.File_ermite_issue_proto.Services().ByName("IssueService").Methods()
184+ issueServiceCreateIssueHandler := connect.NewUnaryHandler(
185+ IssueServiceCreateIssueProcedure,
186+ svc.CreateIssue,
187+ connect.WithSchema(issueServiceMethods.ByName("CreateIssue")),
188+ connect.WithHandlerOptions(opts...),
189+ )
190+ issueServiceListIssuesHandler := connect.NewUnaryHandler(
191+ IssueServiceListIssuesProcedure,
192+ svc.ListIssues,
193+ connect.WithSchema(issueServiceMethods.ByName("ListIssues")),
194+ connect.WithHandlerOptions(opts...),
195+ )
196+ issueServiceGetIssueHandler := connect.NewUnaryHandler(
197+ IssueServiceGetIssueProcedure,
198+ svc.GetIssue,
199+ connect.WithSchema(issueServiceMethods.ByName("GetIssue")),
200+ connect.WithHandlerOptions(opts...),
201+ )
202+ issueServiceAddIssueCommentHandler := connect.NewUnaryHandler(
203+ IssueServiceAddIssueCommentProcedure,
204+ svc.AddIssueComment,
205+ connect.WithSchema(issueServiceMethods.ByName("AddIssueComment")),
206+ connect.WithHandlerOptions(opts...),
207+ )
208+ issueServiceCloseIssueHandler := connect.NewUnaryHandler(
209+ IssueServiceCloseIssueProcedure,
210+ svc.CloseIssue,
211+ connect.WithSchema(issueServiceMethods.ByName("CloseIssue")),
212+ connect.WithHandlerOptions(opts...),
213+ )
214+ issueServiceReopenIssueHandler := connect.NewUnaryHandler(
215+ IssueServiceReopenIssueProcedure,
216+ svc.ReopenIssue,
217+ connect.WithSchema(issueServiceMethods.ByName("ReopenIssue")),
218+ connect.WithHandlerOptions(opts...),
219+ )
220+ return "/ermite.IssueService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
221+ switch r.URL.Path {
222+ case IssueServiceCreateIssueProcedure:
223+ issueServiceCreateIssueHandler.ServeHTTP(w, r)
224+ case IssueServiceListIssuesProcedure:
225+ issueServiceListIssuesHandler.ServeHTTP(w, r)
226+ case IssueServiceGetIssueProcedure:
227+ issueServiceGetIssueHandler.ServeHTTP(w, r)
228+ case IssueServiceAddIssueCommentProcedure:
229+ issueServiceAddIssueCommentHandler.ServeHTTP(w, r)
230+ case IssueServiceCloseIssueProcedure:
231+ issueServiceCloseIssueHandler.ServeHTTP(w, r)
232+ case IssueServiceReopenIssueProcedure:
233+ issueServiceReopenIssueHandler.ServeHTTP(w, r)
234+ default:
235+ http.NotFound(w, r)
236+ }
237+ })
238+}
239+
240+// UnimplementedIssueServiceHandler returns CodeUnimplemented from all methods.
241+type UnimplementedIssueServiceHandler struct{}
242+
243+func (UnimplementedIssueServiceHandler) CreateIssue(context.Context, *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error) {
244+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.CreateIssue is not implemented"))
245+}
246+
247+func (UnimplementedIssueServiceHandler) ListIssues(context.Context, *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error) {
248+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.ListIssues is not implemented"))
249+}
250+
251+func (UnimplementedIssueServiceHandler) GetIssue(context.Context, *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error) {
252+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.GetIssue is not implemented"))
253+}
254+
255+func (UnimplementedIssueServiceHandler) AddIssueComment(context.Context, *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error) {
256+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.AddIssueComment is not implemented"))
257+}
258+
259+func (UnimplementedIssueServiceHandler) CloseIssue(context.Context, *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error) {
260+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.CloseIssue is not implemented"))
261+}
262+
263+func (UnimplementedIssueServiceHandler) ReopenIssue(context.Context, *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error) {
264+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.ReopenIssue is not implemented"))
265+}
@@ -0,0 +1,265 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Code generated by protoc-gen-connect-go. DO NOT EDIT.
4+//
5+// Source: ermite/issue.proto
6+
7+package ermiteconnect
8+
9+import (
10+ ermite "bnema.dev/ermite-api/gen/ermite"
11+ connect "connectrpc.com/connect"
12+ context "context"
13+ errors "errors"
14+ http "net/http"
15+ strings "strings"
16+)
17+
18+// This is a compile-time assertion to ensure that this generated file and the connect package are
19+// compatible. If you get a compiler error that this constant is not defined, this code was
20+// generated with a version of connect newer than the one compiled into your binary. You can fix the
21+// problem by either regenerating this code with an older version of connect or updating the connect
22+// version compiled into your binary.
23+const _ = connect.IsAtLeastVersion1_13_0
24+
25+const (
26+ // IssueServiceName is the fully-qualified name of the IssueService service.
27+ IssueServiceName = "ermite.IssueService"
28+)
29+
30+// These constants are the fully-qualified names of the RPCs defined in this package. They're
31+// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
32+//
33+// Note that these are different from the fully-qualified method names used by
34+// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
35+// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
36+// period.
37+const (
38+ // IssueServiceCreateIssueProcedure is the fully-qualified name of the IssueService's CreateIssue
39+ // RPC.
40+ IssueServiceCreateIssueProcedure = "/ermite.IssueService/CreateIssue"
41+ // IssueServiceListIssuesProcedure is the fully-qualified name of the IssueService's ListIssues RPC.
42+ IssueServiceListIssuesProcedure = "/ermite.IssueService/ListIssues"
43+ // IssueServiceGetIssueProcedure is the fully-qualified name of the IssueService's GetIssue RPC.
44+ IssueServiceGetIssueProcedure = "/ermite.IssueService/GetIssue"
45+ // IssueServiceAddIssueCommentProcedure is the fully-qualified name of the IssueService's
46+ // AddIssueComment RPC.
47+ IssueServiceAddIssueCommentProcedure = "/ermite.IssueService/AddIssueComment"
48+ // IssueServiceCloseIssueProcedure is the fully-qualified name of the IssueService's CloseIssue RPC.
49+ IssueServiceCloseIssueProcedure = "/ermite.IssueService/CloseIssue"
50+ // IssueServiceReopenIssueProcedure is the fully-qualified name of the IssueService's ReopenIssue
51+ // RPC.
52+ IssueServiceReopenIssueProcedure = "/ermite.IssueService/ReopenIssue"
53+)
54+
55+// IssueServiceClient is a client for the ermite.IssueService service.
56+type IssueServiceClient interface {
57+ // CreateIssue opens an issue. Needs the issue:write scope.
58+ CreateIssue(context.Context, *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error)
59+ // ListIssues lists issues, highest number first, optionally in one state.
60+ ListIssues(context.Context, *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error)
61+ // GetIssue returns an issue with its comments.
62+ GetIssue(context.Context, *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error)
63+ // AddIssueComment adds a comment. Needs the issue:write scope.
64+ AddIssueComment(context.Context, *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error)
65+ // CloseIssue closes an issue. Needs the issue:write scope.
66+ CloseIssue(context.Context, *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error)
67+ // ReopenIssue reopens an issue. Needs the issue:write scope.
68+ ReopenIssue(context.Context, *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error)
69+}
70+
71+// NewIssueServiceClient constructs a client for the ermite.IssueService service. By default, it
72+// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
73+// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
74+// connect.WithGRPCWeb() options.
75+//
76+// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
77+// http://api.acme.com or https://acme.com/grpc).
78+func NewIssueServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) IssueServiceClient {
79+ baseURL = strings.TrimRight(baseURL, "/")
80+ issueServiceMethods := ermite.File_ermite_issue_proto.Services().ByName("IssueService").Methods()
81+ return &issueServiceClient{
82+ createIssue: connect.NewClient[ermite.CreateIssueRequest, ermite.CreateIssueResponse](
83+ httpClient,
84+ baseURL+IssueServiceCreateIssueProcedure,
85+ connect.WithSchema(issueServiceMethods.ByName("CreateIssue")),
86+ connect.WithClientOptions(opts...),
87+ ),
88+ listIssues: connect.NewClient[ermite.ListIssuesRequest, ermite.ListIssuesResponse](
89+ httpClient,
90+ baseURL+IssueServiceListIssuesProcedure,
91+ connect.WithSchema(issueServiceMethods.ByName("ListIssues")),
92+ connect.WithClientOptions(opts...),
93+ ),
94+ getIssue: connect.NewClient[ermite.GetIssueRequest, ermite.GetIssueResponse](
95+ httpClient,
96+ baseURL+IssueServiceGetIssueProcedure,
97+ connect.WithSchema(issueServiceMethods.ByName("GetIssue")),
98+ connect.WithClientOptions(opts...),
99+ ),
100+ addIssueComment: connect.NewClient[ermite.AddIssueCommentRequest, ermite.AddIssueCommentResponse](
101+ httpClient,
102+ baseURL+IssueServiceAddIssueCommentProcedure,
103+ connect.WithSchema(issueServiceMethods.ByName("AddIssueComment")),
104+ connect.WithClientOptions(opts...),
105+ ),
106+ closeIssue: connect.NewClient[ermite.CloseIssueRequest, ermite.CloseIssueResponse](
107+ httpClient,
108+ baseURL+IssueServiceCloseIssueProcedure,
109+ connect.WithSchema(issueServiceMethods.ByName("CloseIssue")),
110+ connect.WithClientOptions(opts...),
111+ ),
112+ reopenIssue: connect.NewClient[ermite.ReopenIssueRequest, ermite.ReopenIssueResponse](
113+ httpClient,
114+ baseURL+IssueServiceReopenIssueProcedure,
115+ connect.WithSchema(issueServiceMethods.ByName("ReopenIssue")),
116+ connect.WithClientOptions(opts...),
117+ ),
118+ }
119+}
120+
121+// issueServiceClient implements IssueServiceClient.
122+type issueServiceClient struct {
123+ createIssue *connect.Client[ermite.CreateIssueRequest, ermite.CreateIssueResponse]
124+ listIssues *connect.Client[ermite.ListIssuesRequest, ermite.ListIssuesResponse]
125+ getIssue *connect.Client[ermite.GetIssueRequest, ermite.GetIssueResponse]
126+ addIssueComment *connect.Client[ermite.AddIssueCommentRequest, ermite.AddIssueCommentResponse]
127+ closeIssue *connect.Client[ermite.CloseIssueRequest, ermite.CloseIssueResponse]
128+ reopenIssue *connect.Client[ermite.ReopenIssueRequest, ermite.ReopenIssueResponse]
129+}
130+
131+// CreateIssue calls ermite.IssueService.CreateIssue.
132+func (c *issueServiceClient) CreateIssue(ctx context.Context, req *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error) {
133+ return c.createIssue.CallUnary(ctx, req)
134+}
135+
136+// ListIssues calls ermite.IssueService.ListIssues.
137+func (c *issueServiceClient) ListIssues(ctx context.Context, req *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error) {
138+ return c.listIssues.CallUnary(ctx, req)
139+}
140+
141+// GetIssue calls ermite.IssueService.GetIssue.
142+func (c *issueServiceClient) GetIssue(ctx context.Context, req *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error) {
143+ return c.getIssue.CallUnary(ctx, req)
144+}
145+
146+// AddIssueComment calls ermite.IssueService.AddIssueComment.
147+func (c *issueServiceClient) AddIssueComment(ctx context.Context, req *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error) {
148+ return c.addIssueComment.CallUnary(ctx, req)
149+}
150+
151+// CloseIssue calls ermite.IssueService.CloseIssue.
152+func (c *issueServiceClient) CloseIssue(ctx context.Context, req *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error) {
153+ return c.closeIssue.CallUnary(ctx, req)
154+}
155+
156+// ReopenIssue calls ermite.IssueService.ReopenIssue.
157+func (c *issueServiceClient) ReopenIssue(ctx context.Context, req *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error) {
158+ return c.reopenIssue.CallUnary(ctx, req)
159+}
160+
161+// IssueServiceHandler is an implementation of the ermite.IssueService service.
162+type IssueServiceHandler interface {
163+ // CreateIssue opens an issue. Needs the issue:write scope.
164+ CreateIssue(context.Context, *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error)
165+ // ListIssues lists issues, highest number first, optionally in one state.
166+ ListIssues(context.Context, *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error)
167+ // GetIssue returns an issue with its comments.
168+ GetIssue(context.Context, *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error)
169+ // AddIssueComment adds a comment. Needs the issue:write scope.
170+ AddIssueComment(context.Context, *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error)
171+ // CloseIssue closes an issue. Needs the issue:write scope.
172+ CloseIssue(context.Context, *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error)
173+ // ReopenIssue reopens an issue. Needs the issue:write scope.
174+ ReopenIssue(context.Context, *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error)
175+}
176+
177+// NewIssueServiceHandler builds an HTTP handler from the service implementation. It returns the
178+// path on which to mount the handler and the handler itself.
179+//
180+// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
181+// and JSON codecs. They also support gzip compression.
182+func NewIssueServiceHandler(svc IssueServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
183+ issueServiceMethods := ermite.File_ermite_issue_proto.Services().ByName("IssueService").Methods()
184+ issueServiceCreateIssueHandler := connect.NewUnaryHandler(
185+ IssueServiceCreateIssueProcedure,
186+ svc.CreateIssue,
187+ connect.WithSchema(issueServiceMethods.ByName("CreateIssue")),
188+ connect.WithHandlerOptions(opts...),
189+ )
190+ issueServiceListIssuesHandler := connect.NewUnaryHandler(
191+ IssueServiceListIssuesProcedure,
192+ svc.ListIssues,
193+ connect.WithSchema(issueServiceMethods.ByName("ListIssues")),
194+ connect.WithHandlerOptions(opts...),
195+ )
196+ issueServiceGetIssueHandler := connect.NewUnaryHandler(
197+ IssueServiceGetIssueProcedure,
198+ svc.GetIssue,
199+ connect.WithSchema(issueServiceMethods.ByName("GetIssue")),
200+ connect.WithHandlerOptions(opts...),
201+ )
202+ issueServiceAddIssueCommentHandler := connect.NewUnaryHandler(
203+ IssueServiceAddIssueCommentProcedure,
204+ svc.AddIssueComment,
205+ connect.WithSchema(issueServiceMethods.ByName("AddIssueComment")),
206+ connect.WithHandlerOptions(opts...),
207+ )
208+ issueServiceCloseIssueHandler := connect.NewUnaryHandler(
209+ IssueServiceCloseIssueProcedure,
210+ svc.CloseIssue,
211+ connect.WithSchema(issueServiceMethods.ByName("CloseIssue")),
212+ connect.WithHandlerOptions(opts...),
213+ )
214+ issueServiceReopenIssueHandler := connect.NewUnaryHandler(
215+ IssueServiceReopenIssueProcedure,
216+ svc.ReopenIssue,
217+ connect.WithSchema(issueServiceMethods.ByName("ReopenIssue")),
218+ connect.WithHandlerOptions(opts...),
219+ )
220+ return "/ermite.IssueService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
221+ switch r.URL.Path {
222+ case IssueServiceCreateIssueProcedure:
223+ issueServiceCreateIssueHandler.ServeHTTP(w, r)
224+ case IssueServiceListIssuesProcedure:
225+ issueServiceListIssuesHandler.ServeHTTP(w, r)
226+ case IssueServiceGetIssueProcedure:
227+ issueServiceGetIssueHandler.ServeHTTP(w, r)
228+ case IssueServiceAddIssueCommentProcedure:
229+ issueServiceAddIssueCommentHandler.ServeHTTP(w, r)
230+ case IssueServiceCloseIssueProcedure:
231+ issueServiceCloseIssueHandler.ServeHTTP(w, r)
232+ case IssueServiceReopenIssueProcedure:
233+ issueServiceReopenIssueHandler.ServeHTTP(w, r)
234+ default:
235+ http.NotFound(w, r)
236+ }
237+ })
238+}
239+
240+// UnimplementedIssueServiceHandler returns CodeUnimplemented from all methods.
241+type UnimplementedIssueServiceHandler struct{}
242+
243+func (UnimplementedIssueServiceHandler) CreateIssue(context.Context, *connect.Request[ermite.CreateIssueRequest]) (*connect.Response[ermite.CreateIssueResponse], error) {
244+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.CreateIssue is not implemented"))
245+}
246+
247+func (UnimplementedIssueServiceHandler) ListIssues(context.Context, *connect.Request[ermite.ListIssuesRequest]) (*connect.Response[ermite.ListIssuesResponse], error) {
248+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.ListIssues is not implemented"))
249+}
250+
251+func (UnimplementedIssueServiceHandler) GetIssue(context.Context, *connect.Request[ermite.GetIssueRequest]) (*connect.Response[ermite.GetIssueResponse], error) {
252+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.GetIssue is not implemented"))
253+}
254+
255+func (UnimplementedIssueServiceHandler) AddIssueComment(context.Context, *connect.Request[ermite.AddIssueCommentRequest]) (*connect.Response[ermite.AddIssueCommentResponse], error) {
256+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.AddIssueComment is not implemented"))
257+}
258+
259+func (UnimplementedIssueServiceHandler) CloseIssue(context.Context, *connect.Request[ermite.CloseIssueRequest]) (*connect.Response[ermite.CloseIssueResponse], error) {
260+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.CloseIssue is not implemented"))
261+}
262+
263+func (UnimplementedIssueServiceHandler) ReopenIssue(context.Context, *connect.Request[ermite.ReopenIssueRequest]) (*connect.Response[ermite.ReopenIssueResponse], error) {
264+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ermite.IssueService.ReopenIssue is not implemented"))
265+}
M gen/ermite/event.pb.go +352 -48 generated file: collapsed
A gen/ermite/issue.pb.go +1108 -0 generated file: collapsed
M gen/ermite/repo.pb.go +146 -57 generated file: collapsed
M gen/ermite/token.pb.go +8 -2
@@ -62,6 +62,9 @@ const (
6262 // SCOPE_MR_REVIEW allows setting a verdict (approve, changes requested) on
6363 // merge request notes. Wire name: mr:review.
6464 Scope_SCOPE_MR_REVIEW Scope = 10
65+ // SCOPE_ISSUE_WRITE allows creating, commenting on, closing and reopening
66+ // issues. Wire name: issue:write.
67+ Scope_SCOPE_ISSUE_WRITE Scope = 11
6568 )
6669
6770 // Enum value maps for Scope.
@@ -78,6 +81,7 @@ var (
7881 8: "SCOPE_RUNNER_REGISTER",
7982 9: "SCOPE_TOKEN_ADMIN",
8083 10: "SCOPE_MR_REVIEW",
84+ 11: "SCOPE_ISSUE_WRITE",
8185 }
8286 Scope_value = map[string]int32{
8387 "SCOPE_UNSPECIFIED": 0,
@@ -91,6 +95,7 @@ var (
9195 "SCOPE_RUNNER_REGISTER": 8,
9296 "SCOPE_TOKEN_ADMIN": 9,
9397 "SCOPE_MR_REVIEW": 10,
98+ "SCOPE_ISSUE_WRITE": 11,
9499 }
95100 )
96101
@@ -615,7 +620,7 @@ const file_ermite_token_proto_rawDesc = "" +
615620 "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"$\n" +
616621 "\x12RevokeTokenRequest\x12\x0e\n" +
617622 "\x02id\x18\x01 \x01(\tR\x02id\"\x15\n" +
618- "\x13RevokeTokenResponse*\xfb\x01\n" +
623+ "\x13RevokeTokenResponse*\x92\x02\n" +
619624 "\x05Scope\x12\x15\n" +
620625 "\x11SCOPE_UNSPECIFIED\x10\x00\x12\x13\n" +
621626 "\x0fSCOPE_REPO_READ\x10\x01\x12\x14\n" +
@@ -628,7 +633,8 @@ const file_ermite_token_proto_rawDesc = "" +
628633 "\x15SCOPE_RUNNER_REGISTER\x10\b\x12\x15\n" +
629634 "\x11SCOPE_TOKEN_ADMIN\x10\t\x12\x13\n" +
630635 "\x0fSCOPE_MR_REVIEW\x10\n" +
631- "2\xe3\x01\n" +
636+ "\x12\x15\n" +
637+ "\x11SCOPE_ISSUE_WRITE\x10\v2\xe3\x01\n" +
632638 "\fTokenService\x12F\n" +
633639 "\vCreateToken\x12\x1a.ermite.CreateTokenRequest\x1a\x1b.ermite.CreateTokenResponse\x12C\n" +
634640 "\n" +
@@ -62,6 +62,9 @@ const (
62 // SCOPE_MR_REVIEW allows setting a verdict (approve, changes requested) on62 // SCOPE_MR_REVIEW allows setting a verdict (approve, changes requested) on
63 // merge request notes. Wire name: mr:review.63 // merge request notes. Wire name: mr:review.
64 Scope_SCOPE_MR_REVIEW Scope = 1064 Scope_SCOPE_MR_REVIEW Scope = 10
65+ // SCOPE_ISSUE_WRITE allows creating, commenting on, closing and reopening
66+ // issues. Wire name: issue:write.
67+ Scope_SCOPE_ISSUE_WRITE Scope = 11
65 )68 )
66 69
67 // Enum value maps for Scope.70 // Enum value maps for Scope.
@@ -78,6 +81,7 @@ var (
78 8: "SCOPE_RUNNER_REGISTER",81 8: "SCOPE_RUNNER_REGISTER",
79 9: "SCOPE_TOKEN_ADMIN",82 9: "SCOPE_TOKEN_ADMIN",
80 10: "SCOPE_MR_REVIEW",83 10: "SCOPE_MR_REVIEW",
84+ 11: "SCOPE_ISSUE_WRITE",
81 }85 }
82 Scope_value = map[string]int32{86 Scope_value = map[string]int32{
83 "SCOPE_UNSPECIFIED": 0,87 "SCOPE_UNSPECIFIED": 0,
@@ -91,6 +95,7 @@ var (
91 "SCOPE_RUNNER_REGISTER": 8,95 "SCOPE_RUNNER_REGISTER": 8,
92 "SCOPE_TOKEN_ADMIN": 9,96 "SCOPE_TOKEN_ADMIN": 9,
93 "SCOPE_MR_REVIEW": 10,97 "SCOPE_MR_REVIEW": 10,
98+ "SCOPE_ISSUE_WRITE": 11,
94 }99 }
95 )100 )
96 101
@@ -615,7 +620,7 @@ const file_ermite_token_proto_rawDesc = "" +
615 "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"$\n" +620 "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"$\n" +
616 "\x12RevokeTokenRequest\x12\x0e\n" +621 "\x12RevokeTokenRequest\x12\x0e\n" +
617 "\x02id\x18\x01 \x01(\tR\x02id\"\x15\n" +622 "\x02id\x18\x01 \x01(\tR\x02id\"\x15\n" +
618- "\x13RevokeTokenResponse*\xfb\x01\n" +623+ "\x13RevokeTokenResponse*\x92\x02\n" +
619 "\x05Scope\x12\x15\n" +624 "\x05Scope\x12\x15\n" +
620 "\x11SCOPE_UNSPECIFIED\x10\x00\x12\x13\n" +625 "\x11SCOPE_UNSPECIFIED\x10\x00\x12\x13\n" +
621 "\x0fSCOPE_REPO_READ\x10\x01\x12\x14\n" +626 "\x0fSCOPE_REPO_READ\x10\x01\x12\x14\n" +
@@ -628,7 +633,8 @@ const file_ermite_token_proto_rawDesc = "" +
628 "\x15SCOPE_RUNNER_REGISTER\x10\b\x12\x15\n" +633 "\x15SCOPE_RUNNER_REGISTER\x10\b\x12\x15\n" +
629 "\x11SCOPE_TOKEN_ADMIN\x10\t\x12\x13\n" +634 "\x11SCOPE_TOKEN_ADMIN\x10\t\x12\x13\n" +
630 "\x0fSCOPE_MR_REVIEW\x10\n" +635 "\x0fSCOPE_MR_REVIEW\x10\n" +
631- "2\xe3\x01\n" +636+ "\x12\x15\n" +
637+ "\x11SCOPE_ISSUE_WRITE\x10\v2\xe3\x01\n" +
632 "\fTokenService\x12F\n" +638 "\fTokenService\x12F\n" +
633 "\vCreateToken\x12\x1a.ermite.CreateTokenRequest\x1a\x1b.ermite.CreateTokenResponse\x12C\n" +639 "\vCreateToken\x12\x1a.ermite.CreateTokenRequest\x1a\x1b.ermite.CreateTokenResponse\x12C\n" +
634 "\n" +640 "\n" +
A issue_test.go +85 -0
@@ -0,0 +1,85 @@
1+// SPDX-License-Identifier: MIT
2+
3+package api_test
4+
5+import (
6+ "testing"
7+
8+ "bnema.dev/ermite-api/gen/ermite"
9+ "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
10+ "google.golang.org/protobuf/proto"
11+ "google.golang.org/protobuf/reflect/protoreflect"
12+)
13+
14+func TestIssueProcedures(t *testing.T) {
15+ tests := map[string]string{
16+ ermiteconnect.IssueServiceCreateIssueProcedure: "CreateIssue",
17+ ermiteconnect.IssueServiceListIssuesProcedure: "ListIssues",
18+ ermiteconnect.IssueServiceGetIssueProcedure: "GetIssue",
19+ ermiteconnect.IssueServiceAddIssueCommentProcedure: "AddIssueComment",
20+ ermiteconnect.IssueServiceCloseIssueProcedure: "CloseIssue",
21+ ermiteconnect.IssueServiceReopenIssueProcedure: "ReopenIssue",
22+ }
23+ for got, name := range tests {
24+ if want := "/ermite.IssueService/" + name; got != want {
25+ t.Errorf("procedure = %q, want %q", got, want)
26+ }
27+ }
28+}
29+
30+func TestIssueContract(t *testing.T) {
31+ for _, tt := range []struct {
32+ msg proto.Message
33+ fields map[string]int
34+ }{
35+ {&ermite.Issue{}, map[string]int{"repo": 1, "number": 2, "title": 3, "body": 4, "state": 5, "author": 6, "created_at": 7, "updated_at": 8, "comment_count": 9}},
36+ {&ermite.IssueComment{}, map[string]int{"id": 1, "author": 2, "text": 3, "created_at": 4}},
37+ {&ermite.RepoSettings{}, map[string]int{"issues": 3}},
38+ {&ermite.UpdateRepoRequest{}, map[string]int{"issues": 8}},
39+ {&ermite.Event{}, map[string]int{"issue_opened": 17, "issue_closed": 18, "issue_reopened": 19, "issue_commented": 20}},
40+ } {
41+ md := tt.msg.ProtoReflect().Descriptor()
42+ for name, num := range tt.fields {
43+ if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
44+ t.Errorf("%s.%s = %v, want %d", md.Name(), name, fd, num)
45+ }
46+ }
47+ }
48+ if ermite.Scope_SCOPE_ISSUE_WRITE != 11 {
49+ t.Fatal("issue:write scope number changed")
50+ }
51+ for name, want := range map[string]int32{"ISSUE_STATE_UNSPECIFIED": 0, "ISSUE_STATE_OPEN": 1, "ISSUE_STATE_CLOSED": 2, "ISSUE_STATE_PENDING": 3} {
52+ if got := ermite.IssueState_value[name]; got != want {
53+ t.Errorf("%s = %d, want %d", name, got, want)
54+ }
55+ }
56+ for name, want := range map[string]int32{"ISSUES_MODE_UNSPECIFIED": 0, "ISSUES_MODE_OFF": 1, "ISSUES_MODE_OWNER": 2, "ISSUES_MODE_PUBLIC": 3} {
57+ if got := ermite.IssuesMode_value[name]; got != want {
58+ t.Errorf("%s = %d, want %d", name, got, want)
59+ }
60+ }
61+ mode := ermite.IssuesMode_ISSUES_MODE_OFF
62+ for _, in := range []proto.Message{
63+ &ermite.UpdateRepoRequest{Repo: "r", Issues: &mode},
64+ &ermite.Event{Kind: &ermite.Event_IssueOpened{IssueOpened: &ermite.IssueOpened{Issue: &ermite.Issue{Repo: "r", Number: 1}}}},
65+ &ermite.Event{Kind: &ermite.Event_IssueClosed{IssueClosed: &ermite.IssueClosed{Issue: &ermite.Issue{Repo: "r", Number: 1}}}},
66+ &ermite.Event{Kind: &ermite.Event_IssueReopened{IssueReopened: &ermite.IssueReopened{Issue: &ermite.Issue{Repo: "r", Number: 1}}}},
67+ &ermite.Event{Kind: &ermite.Event_IssueCommented{IssueCommented: &ermite.IssueCommented{Repo: "r", Number: 1, Comment: &ermite.IssueComment{Id: "c", Text: "hi"}}}},
68+ } {
69+ b, err := proto.Marshal(in)
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ out := in.ProtoReflect().New().Interface()
74+ if err := proto.Unmarshal(b, out); err != nil {
75+ t.Fatal(err)
76+ }
77+ if !proto.Equal(in, out) {
78+ t.Errorf("%T: round trip mismatch", in)
79+ }
80+ }
81+ fd := (&ermite.UpdateRepoRequest{}).ProtoReflect().Descriptor().Fields().ByName("issues")
82+ if !fd.HasOptionalKeyword() {
83+ t.Fatal("UpdateRepoRequest.issues must be optional")
84+ }
85+}
@@ -0,0 +1,85 @@
1+// SPDX-License-Identifier: MIT
2+
3+package api_test
4+
5+import (
6+ "testing"
7+
8+ "bnema.dev/ermite-api/gen/ermite"
9+ "bnema.dev/ermite-api/gen/ermite/ermiteconnect"
10+ "google.golang.org/protobuf/proto"
11+ "google.golang.org/protobuf/reflect/protoreflect"
12+)
13+
14+func TestIssueProcedures(t *testing.T) {
15+ tests := map[string]string{
16+ ermiteconnect.IssueServiceCreateIssueProcedure: "CreateIssue",
17+ ermiteconnect.IssueServiceListIssuesProcedure: "ListIssues",
18+ ermiteconnect.IssueServiceGetIssueProcedure: "GetIssue",
19+ ermiteconnect.IssueServiceAddIssueCommentProcedure: "AddIssueComment",
20+ ermiteconnect.IssueServiceCloseIssueProcedure: "CloseIssue",
21+ ermiteconnect.IssueServiceReopenIssueProcedure: "ReopenIssue",
22+ }
23+ for got, name := range tests {
24+ if want := "/ermite.IssueService/" + name; got != want {
25+ t.Errorf("procedure = %q, want %q", got, want)
26+ }
27+ }
28+}
29+
30+func TestIssueContract(t *testing.T) {
31+ for _, tt := range []struct {
32+ msg proto.Message
33+ fields map[string]int
34+ }{
35+ {&ermite.Issue{}, map[string]int{"repo": 1, "number": 2, "title": 3, "body": 4, "state": 5, "author": 6, "created_at": 7, "updated_at": 8, "comment_count": 9}},
36+ {&ermite.IssueComment{}, map[string]int{"id": 1, "author": 2, "text": 3, "created_at": 4}},
37+ {&ermite.RepoSettings{}, map[string]int{"issues": 3}},
38+ {&ermite.UpdateRepoRequest{}, map[string]int{"issues": 8}},
39+ {&ermite.Event{}, map[string]int{"issue_opened": 17, "issue_closed": 18, "issue_reopened": 19, "issue_commented": 20}},
40+ } {
41+ md := tt.msg.ProtoReflect().Descriptor()
42+ for name, num := range tt.fields {
43+ if fd := md.Fields().ByName(protoreflect.Name(name)); fd == nil || int(fd.Number()) != num {
44+ t.Errorf("%s.%s = %v, want %d", md.Name(), name, fd, num)
45+ }
46+ }
47+ }
48+ if ermite.Scope_SCOPE_ISSUE_WRITE != 11 {
49+ t.Fatal("issue:write scope number changed")
50+ }
51+ for name, want := range map[string]int32{"ISSUE_STATE_UNSPECIFIED": 0, "ISSUE_STATE_OPEN": 1, "ISSUE_STATE_CLOSED": 2, "ISSUE_STATE_PENDING": 3} {
52+ if got := ermite.IssueState_value[name]; got != want {
53+ t.Errorf("%s = %d, want %d", name, got, want)
54+ }
55+ }
56+ for name, want := range map[string]int32{"ISSUES_MODE_UNSPECIFIED": 0, "ISSUES_MODE_OFF": 1, "ISSUES_MODE_OWNER": 2, "ISSUES_MODE_PUBLIC": 3} {
57+ if got := ermite.IssuesMode_value[name]; got != want {
58+ t.Errorf("%s = %d, want %d", name, got, want)
59+ }
60+ }
61+ mode := ermite.IssuesMode_ISSUES_MODE_OFF
62+ for _, in := range []proto.Message{
63+ &ermite.UpdateRepoRequest{Repo: "r", Issues: &mode},
64+ &ermite.Event{Kind: &ermite.Event_IssueOpened{IssueOpened: &ermite.IssueOpened{Issue: &ermite.Issue{Repo: "r", Number: 1}}}},
65+ &ermite.Event{Kind: &ermite.Event_IssueClosed{IssueClosed: &ermite.IssueClosed{Issue: &ermite.Issue{Repo: "r", Number: 1}}}},
66+ &ermite.Event{Kind: &ermite.Event_IssueReopened{IssueReopened: &ermite.IssueReopened{Issue: &ermite.Issue{Repo: "r", Number: 1}}}},
67+ &ermite.Event{Kind: &ermite.Event_IssueCommented{IssueCommented: &ermite.IssueCommented{Repo: "r", Number: 1, Comment: &ermite.IssueComment{Id: "c", Text: "hi"}}}},
68+ } {
69+ b, err := proto.Marshal(in)
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ out := in.ProtoReflect().New().Interface()
74+ if err := proto.Unmarshal(b, out); err != nil {
75+ t.Fatal(err)
76+ }
77+ if !proto.Equal(in, out) {
78+ t.Errorf("%T: round trip mismatch", in)
79+ }
80+ }
81+ fd := (&ermite.UpdateRepoRequest{}).ProtoReflect().Descriptor().Fields().ByName("issues")
82+ if !fd.HasOptionalKeyword() {
83+ t.Fatal("UpdateRepoRequest.issues must be optional")
84+ }
85+}
M proto/ermite/event.proto +41 -2
@@ -4,6 +4,7 @@ syntax = "proto3";
44
55 package ermite;
66
7+import "ermite/issue.proto";
78 import "ermite/merge_request.proto";
89 import "ermite/repo.proto";
910 import "ermite/run.proto";
@@ -18,7 +19,8 @@ service EventService {
1819 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,
1920 // for merge requests, merge_request_opened, merge_request_merged,
2021 // merge_request_closed, comment_added, comment_edited, comment_deleted and
21- // comment_resolved (a thread resolved or reopened). Each one is delivered on the
22+ // comment_resolved (a thread resolved or reopened); for issues, issue_opened,
23+ // issue_closed, issue_reopened and issue_commented. Each one is delivered on the
2224 // "repo:<name>" topic of its repository and on "home", subject to the
2325 // visibility of the repository.
2426 //
@@ -109,6 +111,14 @@ message Event {
109111 CommentDeleted comment_deleted = 15;
110112 // CommentResolved: a merge request comment was resolved or unresolved.
111113 CommentResolved comment_resolved = 16;
114+ // IssueOpened: an issue was opened.
115+ IssueOpened issue_opened = 17;
116+ // IssueClosed: an issue was closed.
117+ IssueClosed issue_closed = 18;
118+ // IssueReopened: an issue was reopened.
119+ IssueReopened issue_reopened = 19;
120+ // IssueCommented: a comment was added to an issue.
121+ IssueCommented issue_commented = 20;
112122 }
113123 }
114124
@@ -209,10 +219,39 @@ message CommentResolved {
209219 Comment comment = 3;
210220 }
211221
222+// IssueOpened reports a new issue.
223+message IssueOpened {
224+ // Issue is the new issue, including its repository and number.
225+ Issue issue = 1;
226+}
227+
228+// IssueClosed reports a closed issue.
229+message IssueClosed {
230+ // Issue is the issue after closing, including its repository and number.
231+ Issue issue = 1;
232+}
233+
234+// IssueReopened reports a reopened issue.
235+message IssueReopened {
236+ // Issue is the issue after reopening, including its repository and number.
237+ Issue issue = 1;
238+}
239+
240+// IssueCommented reports a new comment on an issue.
241+message IssueCommented {
242+ // Repo is the name of the repository.
243+ string repo = 1;
244+ // Number is the number of the issue.
245+ int64 number = 2;
246+ // Comment is the new comment.
247+ IssueComment comment = 3;
248+}
249+
212250 // Gap tells the client that events were lost: the requested resume point is
213251 // older than the server's buffer. It has no fields. The client should reload
214252 // the state it displays (repositories, refs, merge requests and their
215-// comments, CI runs and their jobs) and carry on with the next events.
253+// comments, issues and their comments, CI runs and their jobs) and carry on
254+// with the next events.
216255 message Gap {}
217256
218257 // RunUpdated reports a CI run that was created or whose status, conclusion or
@@ -4,6 +4,7 @@ syntax = "proto3";
4 4
5 package ermite;5 package ermite;
6 6
7+import "ermite/issue.proto";
7 import "ermite/merge_request.proto";8 import "ermite/merge_request.proto";
8 import "ermite/repo.proto";9 import "ermite/repo.proto";
9 import "ermite/run.proto";10 import "ermite/run.proto";
@@ -18,7 +19,8 @@ service EventService {
18 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,19 // The events are repo_created, repo_updated, repo_deleted, pushed, gap and,
19 // for merge requests, merge_request_opened, merge_request_merged,20 // for merge requests, merge_request_opened, merge_request_merged,
20 // merge_request_closed, comment_added, comment_edited, comment_deleted and21 // merge_request_closed, comment_added, comment_edited, comment_deleted and
21- // comment_resolved (a thread resolved or reopened). Each one is delivered on the22+ // comment_resolved (a thread resolved or reopened); for issues, issue_opened,
23+ // issue_closed, issue_reopened and issue_commented. Each one is delivered on the
22 // "repo:<name>" topic of its repository and on "home", subject to the24 // "repo:<name>" topic of its repository and on "home", subject to the
23 // visibility of the repository.25 // visibility of the repository.
24 //26 //
@@ -109,6 +111,14 @@ message Event {
109 CommentDeleted comment_deleted = 15;111 CommentDeleted comment_deleted = 15;
110 // CommentResolved: a merge request comment was resolved or unresolved.112 // CommentResolved: a merge request comment was resolved or unresolved.
111 CommentResolved comment_resolved = 16;113 CommentResolved comment_resolved = 16;
114+ // IssueOpened: an issue was opened.
115+ IssueOpened issue_opened = 17;
116+ // IssueClosed: an issue was closed.
117+ IssueClosed issue_closed = 18;
118+ // IssueReopened: an issue was reopened.
119+ IssueReopened issue_reopened = 19;
120+ // IssueCommented: a comment was added to an issue.
121+ IssueCommented issue_commented = 20;
112 }122 }
113 }123 }
114 124
@@ -209,10 +219,39 @@ message CommentResolved {
209 Comment comment = 3;219 Comment comment = 3;
210 }220 }
211 221
222+// IssueOpened reports a new issue.
223+message IssueOpened {
224+ // Issue is the new issue, including its repository and number.
225+ Issue issue = 1;
226+}
227+
228+// IssueClosed reports a closed issue.
229+message IssueClosed {
230+ // Issue is the issue after closing, including its repository and number.
231+ Issue issue = 1;
232+}
233+
234+// IssueReopened reports a reopened issue.
235+message IssueReopened {
236+ // Issue is the issue after reopening, including its repository and number.
237+ Issue issue = 1;
238+}
239+
240+// IssueCommented reports a new comment on an issue.
241+message IssueCommented {
242+ // Repo is the name of the repository.
243+ string repo = 1;
244+ // Number is the number of the issue.
245+ int64 number = 2;
246+ // Comment is the new comment.
247+ IssueComment comment = 3;
248+}
249+
212 // Gap tells the client that events were lost: the requested resume point is250 // Gap tells the client that events were lost: the requested resume point is
213 // older than the server's buffer. It has no fields. The client should reload251 // older than the server's buffer. It has no fields. The client should reload
214 // the state it displays (repositories, refs, merge requests and their252 // the state it displays (repositories, refs, merge requests and their
215-// comments, CI runs and their jobs) and carry on with the next events.253+// comments, issues and their comments, CI runs and their jobs) and carry on
254+// with the next events.
216 message Gap {}255 message Gap {}
217 256
218 // RunUpdated reports a CI run that was created or whose status, conclusion or257 // RunUpdated reports a CI run that was created or whose status, conclusion or
A proto/ermite/issue.proto +172 -0
@@ -0,0 +1,172 @@
1+// SPDX-License-Identifier: MIT
2+
3+syntax = "proto3";
4+
5+package ermite;
6+
7+import "google/protobuf/timestamp.proto";
8+
9+// IssueState is where an issue is in its life.
10+enum IssueState {
11+ // Not set. In ListIssuesRequest.state it means every state. Never returned
12+ // by the server.
13+ ISSUE_STATE_UNSPECIFIED = 0;
14+ // The issue is open.
15+ ISSUE_STATE_OPEN = 1;
16+ // The issue is closed.
17+ ISSUE_STATE_CLOSED = 2;
18+ // Reserved for later use: awaiting moderation, owner-only.
19+ ISSUE_STATE_PENDING = 3;
20+}
21+
22+// Issue is a discussion in a repository.
23+message Issue {
24+ // Repo is the name of the repository.
25+ string repo = 1;
26+ // Number identifies the issue within its repository.
27+ int64 number = 2;
28+ // Title is the one-line title.
29+ string title = 3;
30+ // Body is the content of the issue. It may be empty.
31+ string body = 4;
32+ // State is where the issue is in its life.
33+ IssueState state = 5;
34+ // Author names who created the issue: the label of the token that was used,
35+ // "root" for the root token, or "owner" for a web session. It is
36+ // informational: labels are not unique and the token may be gone.
37+ string author = 6;
38+ // CreatedAt is when the issue was created.
39+ google.protobuf.Timestamp created_at = 7;
40+ // UpdatedAt is when the issue last changed.
41+ google.protobuf.Timestamp updated_at = 8;
42+ // CommentCount is the number of comments on the issue.
43+ int32 comment_count = 9;
44+}
45+
46+// IssueComment is a note on an issue.
47+message IssueComment {
48+ // Id identifies the comment within its issue. Opaque.
49+ string id = 1;
50+ // Author names who wrote the comment, like Issue.author.
51+ string author = 2;
52+ // Text is the content of the comment.
53+ string text = 3;
54+ // CreatedAt is when the comment was written.
55+ google.protobuf.Timestamp created_at = 4;
56+}
57+
58+// IssueService manages issues and their comments.
59+service IssueService {
60+ // CreateIssue opens an issue. Needs the issue:write scope.
61+ rpc CreateIssue(CreateIssueRequest) returns (CreateIssueResponse);
62+ // ListIssues lists issues, highest number first, optionally in one state.
63+ rpc ListIssues(ListIssuesRequest) returns (ListIssuesResponse);
64+ // GetIssue returns an issue with its comments.
65+ rpc GetIssue(GetIssueRequest) returns (GetIssueResponse);
66+ // AddIssueComment adds a comment. Needs the issue:write scope.
67+ rpc AddIssueComment(AddIssueCommentRequest) returns (AddIssueCommentResponse);
68+ // CloseIssue closes an issue. Needs the issue:write scope.
69+ rpc CloseIssue(CloseIssueRequest) returns (CloseIssueResponse);
70+ // ReopenIssue reopens an issue. Needs the issue:write scope.
71+ rpc ReopenIssue(ReopenIssueRequest) returns (ReopenIssueResponse);
72+}
73+
74+// CreateIssueRequest describes the issue to open.
75+message CreateIssueRequest {
76+ // Repo is the repository name.
77+ string repo = 1;
78+ // Title is the one-line title. Required.
79+ string title = 2;
80+ // Body is the optional content.
81+ string body = 3;
82+}
83+
84+// CreateIssueResponse holds the new issue.
85+message CreateIssueResponse {
86+ // Issue is the created issue.
87+ Issue issue = 1;
88+}
89+
90+// ListIssuesRequest selects a page of issues.
91+message ListIssuesRequest {
92+ // Repo is the repository name.
93+ string repo = 1;
94+ // State restricts the result to one state. ISSUE_STATE_UNSPECIFIED means
95+ // every state.
96+ IssueState state = 2;
97+ // PageSize is the maximum number of issues to return. 0 means the server
98+ // default; values above the server maximum are lowered to it.
99+ int32 page_size = 3;
100+ // PageToken is the next_page_token of a previous response, to continue the
101+ // same listing. Empty for the first page. Other fields must be unchanged.
102+ string page_token = 4;
103+}
104+
105+// ListIssuesResponse is one page of issues.
106+message ListIssuesResponse {
107+ // Issues are the issues of this page, highest number first.
108+ repeated Issue issues = 1;
109+ // NextPageToken is passed as page_token to get the next page. Empty when
110+ // there is no next page.
111+ string next_page_token = 2;
112+}
113+
114+// GetIssueRequest selects an issue.
115+message GetIssueRequest {
116+ // Repo is the repository name.
117+ string repo = 1;
118+ // Number is the issue number.
119+ int64 number = 2;
120+}
121+
122+// GetIssueResponse holds an issue with its comments.
123+message GetIssueResponse {
124+ // Issue is the issue.
125+ Issue issue = 1;
126+ // Comments are every comment of the issue, oldest first.
127+ repeated IssueComment comments = 2;
128+}
129+
130+// AddIssueCommentRequest describes the comment to add.
131+message AddIssueCommentRequest {
132+ // Repo is the repository name.
133+ string repo = 1;
134+ // Number is the issue number.
135+ int64 number = 2;
136+ // Text is the content. Required.
137+ string text = 3;
138+}
139+
140+// AddIssueCommentResponse holds the new comment.
141+message AddIssueCommentResponse {
142+ // Comment is the created comment.
143+ IssueComment comment = 1;
144+}
145+
146+// CloseIssueRequest names the issue to close.
147+message CloseIssueRequest {
148+ // Repo is the repository name.
149+ string repo = 1;
150+ // Number is the issue number.
151+ int64 number = 2;
152+}
153+
154+// CloseIssueResponse holds the issue after closing.
155+message CloseIssueResponse {
156+ // Issue is the closed issue.
157+ Issue issue = 1;
158+}
159+
160+// ReopenIssueRequest names the issue to reopen.
161+message ReopenIssueRequest {
162+ // Repo is the repository name.
163+ string repo = 1;
164+ // Number is the issue number.
165+ int64 number = 2;
166+}
167+
168+// ReopenIssueResponse holds the issue after reopening.
169+message ReopenIssueResponse {
170+ // Issue is the reopened issue.
171+ Issue issue = 1;
172+}
@@ -0,0 +1,172 @@
1+// SPDX-License-Identifier: MIT
2+
3+syntax = "proto3";
4+
5+package ermite;
6+
7+import "google/protobuf/timestamp.proto";
8+
9+// IssueState is where an issue is in its life.
10+enum IssueState {
11+ // Not set. In ListIssuesRequest.state it means every state. Never returned
12+ // by the server.
13+ ISSUE_STATE_UNSPECIFIED = 0;
14+ // The issue is open.
15+ ISSUE_STATE_OPEN = 1;
16+ // The issue is closed.
17+ ISSUE_STATE_CLOSED = 2;
18+ // Reserved for later use: awaiting moderation, owner-only.
19+ ISSUE_STATE_PENDING = 3;
20+}
21+
22+// Issue is a discussion in a repository.
23+message Issue {
24+ // Repo is the name of the repository.
25+ string repo = 1;
26+ // Number identifies the issue within its repository.
27+ int64 number = 2;
28+ // Title is the one-line title.
29+ string title = 3;
30+ // Body is the content of the issue. It may be empty.
31+ string body = 4;
32+ // State is where the issue is in its life.
33+ IssueState state = 5;
34+ // Author names who created the issue: the label of the token that was used,
35+ // "root" for the root token, or "owner" for a web session. It is
36+ // informational: labels are not unique and the token may be gone.
37+ string author = 6;
38+ // CreatedAt is when the issue was created.
39+ google.protobuf.Timestamp created_at = 7;
40+ // UpdatedAt is when the issue last changed.
41+ google.protobuf.Timestamp updated_at = 8;
42+ // CommentCount is the number of comments on the issue.
43+ int32 comment_count = 9;
44+}
45+
46+// IssueComment is a note on an issue.
47+message IssueComment {
48+ // Id identifies the comment within its issue. Opaque.
49+ string id = 1;
50+ // Author names who wrote the comment, like Issue.author.
51+ string author = 2;
52+ // Text is the content of the comment.
53+ string text = 3;
54+ // CreatedAt is when the comment was written.
55+ google.protobuf.Timestamp created_at = 4;
56+}
57+
58+// IssueService manages issues and their comments.
59+service IssueService {
60+ // CreateIssue opens an issue. Needs the issue:write scope.
61+ rpc CreateIssue(CreateIssueRequest) returns (CreateIssueResponse);
62+ // ListIssues lists issues, highest number first, optionally in one state.
63+ rpc ListIssues(ListIssuesRequest) returns (ListIssuesResponse);
64+ // GetIssue returns an issue with its comments.
65+ rpc GetIssue(GetIssueRequest) returns (GetIssueResponse);
66+ // AddIssueComment adds a comment. Needs the issue:write scope.
67+ rpc AddIssueComment(AddIssueCommentRequest) returns (AddIssueCommentResponse);
68+ // CloseIssue closes an issue. Needs the issue:write scope.
69+ rpc CloseIssue(CloseIssueRequest) returns (CloseIssueResponse);
70+ // ReopenIssue reopens an issue. Needs the issue:write scope.
71+ rpc ReopenIssue(ReopenIssueRequest) returns (ReopenIssueResponse);
72+}
73+
74+// CreateIssueRequest describes the issue to open.
75+message CreateIssueRequest {
76+ // Repo is the repository name.
77+ string repo = 1;
78+ // Title is the one-line title. Required.
79+ string title = 2;
80+ // Body is the optional content.
81+ string body = 3;
82+}
83+
84+// CreateIssueResponse holds the new issue.
85+message CreateIssueResponse {
86+ // Issue is the created issue.
87+ Issue issue = 1;
88+}
89+
90+// ListIssuesRequest selects a page of issues.
91+message ListIssuesRequest {
92+ // Repo is the repository name.
93+ string repo = 1;
94+ // State restricts the result to one state. ISSUE_STATE_UNSPECIFIED means
95+ // every state.
96+ IssueState state = 2;
97+ // PageSize is the maximum number of issues to return. 0 means the server
98+ // default; values above the server maximum are lowered to it.
99+ int32 page_size = 3;
100+ // PageToken is the next_page_token of a previous response, to continue the
101+ // same listing. Empty for the first page. Other fields must be unchanged.
102+ string page_token = 4;
103+}
104+
105+// ListIssuesResponse is one page of issues.
106+message ListIssuesResponse {
107+ // Issues are the issues of this page, highest number first.
108+ repeated Issue issues = 1;
109+ // NextPageToken is passed as page_token to get the next page. Empty when
110+ // there is no next page.
111+ string next_page_token = 2;
112+}
113+
114+// GetIssueRequest selects an issue.
115+message GetIssueRequest {
116+ // Repo is the repository name.
117+ string repo = 1;
118+ // Number is the issue number.
119+ int64 number = 2;
120+}
121+
122+// GetIssueResponse holds an issue with its comments.
123+message GetIssueResponse {
124+ // Issue is the issue.
125+ Issue issue = 1;
126+ // Comments are every comment of the issue, oldest first.
127+ repeated IssueComment comments = 2;
128+}
129+
130+// AddIssueCommentRequest describes the comment to add.
131+message AddIssueCommentRequest {
132+ // Repo is the repository name.
133+ string repo = 1;
134+ // Number is the issue number.
135+ int64 number = 2;
136+ // Text is the content. Required.
137+ string text = 3;
138+}
139+
140+// AddIssueCommentResponse holds the new comment.
141+message AddIssueCommentResponse {
142+ // Comment is the created comment.
143+ IssueComment comment = 1;
144+}
145+
146+// CloseIssueRequest names the issue to close.
147+message CloseIssueRequest {
148+ // Repo is the repository name.
149+ string repo = 1;
150+ // Number is the issue number.
151+ int64 number = 2;
152+}
153+
154+// CloseIssueResponse holds the issue after closing.
155+message CloseIssueResponse {
156+ // Issue is the closed issue.
157+ Issue issue = 1;
158+}
159+
160+// ReopenIssueRequest names the issue to reopen.
161+message ReopenIssueRequest {
162+ // Repo is the repository name.
163+ string repo = 1;
164+ // Number is the issue number.
165+ int64 number = 2;
166+}
167+
168+// ReopenIssueResponse holds the issue after reopening.
169+message ReopenIssueResponse {
170+ // Issue is the reopened issue.
171+ Issue issue = 1;
172+}
M proto/ermite/repo.proto +21 -4
@@ -20,8 +20,20 @@ enum Visibility {
2020 VISIBILITY_PRIVATE = 2;
2121 }
2222
23+// IssuesMode says who may write issues in a repository.
24+enum IssuesMode {
25+ // Not set.
26+ ISSUES_MODE_UNSPECIFIED = 0;
27+ // Issues are disabled.
28+ ISSUES_MODE_OFF = 1;
29+ // Only the owner may write issues.
30+ ISSUES_MODE_OWNER = 2;
31+ // Issues are open to public participation.
32+ ISSUES_MODE_PUBLIC = 3;
33+}
34+
2335 // RepoSettings are the settings of a repository that govern its merge
24-// requests.
36+// requests and issues.
2537 message RepoSettings {
2638 // AllowedMergeModes are the modes with which a merge request of the
2739 // repository may be merged. It is never empty in a response and has no
@@ -33,6 +45,8 @@ message RepoSettings {
3345 // explicitly, so a later change of the server default never alters an
3446 // existing repository.
3547 bool failing_checks_block = 2;
48+ // Issues is the issue participation mode of the repository.
49+ IssuesMode issues = 3;
3650 }
3751
3852 // Repo is the metadata of a repository. Git-owned state (refs, trees, history)
@@ -53,7 +67,7 @@ message Repo {
5367 google.protobuf.Timestamp created_at = 5;
5468 // PushedAt is the time of the last push. It is unset until the first push.
5569 google.protobuf.Timestamp pushed_at = 6;
56- // Settings are the merge settings of the repository. Always set in a
70+ // Settings are the settings of the repository. Always set in a
5771 // response, with the defaults for a repository that never changed them.
5872 RepoSettings settings = 7;
5973 // Topics classify the repository, as on GitHub: at most 20, each 1-50
@@ -153,8 +167,9 @@ message UpdateRepoRequest {
153167 string default_branch = 4;
154168 // UpdateMask lists the fields to change: "description", "visibility",
155169 // "default_branch", "topics", "settings.allowed_merge_modes" and/or
156- // "settings.failing_checks_block". "settings" stands for both settings
157- // paths. It must not be empty. Other paths are invalid_argument.
170+ // "settings.failing_checks_block". "settings" stands for both merge settings
171+ // paths. The optional issues field changes the issue mode separately.
172+ // It must not be empty. Other paths are invalid_argument.
158173 google.protobuf.FieldMask update_mask = 5;
159174 // Settings holds the new merge settings. Only the fields named in
160175 // update_mask are used. allowed_merge_modes must then not be empty nor
@@ -165,6 +180,8 @@ message UpdateRepoRequest {
165180 // Topics replace the topics of the repository (see Repo.topics). Used only
166181 // if update_mask contains "topics"; an empty list then clears them.
167182 repeated string topics = 7;
183+ // Issues is the new issue participation mode. When absent it is unchanged.
184+ optional IssuesMode issues = 8;
168185 }
169186
170187 // UpdateRepoResponse holds the repository after the update.
@@ -20,8 +20,20 @@ enum Visibility {
20 VISIBILITY_PRIVATE = 2;20 VISIBILITY_PRIVATE = 2;
21 }21 }
22 22
23+// IssuesMode says who may write issues in a repository.
24+enum IssuesMode {
25+ // Not set.
26+ ISSUES_MODE_UNSPECIFIED = 0;
27+ // Issues are disabled.
28+ ISSUES_MODE_OFF = 1;
29+ // Only the owner may write issues.
30+ ISSUES_MODE_OWNER = 2;
31+ // Issues are open to public participation.
32+ ISSUES_MODE_PUBLIC = 3;
33+}
34+
23 // RepoSettings are the settings of a repository that govern its merge35 // RepoSettings are the settings of a repository that govern its merge
24-// requests.36+// requests and issues.
25 message RepoSettings {37 message RepoSettings {
26 // AllowedMergeModes are the modes with which a merge request of the38 // AllowedMergeModes are the modes with which a merge request of the
27 // repository may be merged. It is never empty in a response and has no39 // repository may be merged. It is never empty in a response and has no
@@ -33,6 +45,8 @@ message RepoSettings {
33 // explicitly, so a later change of the server default never alters an45 // explicitly, so a later change of the server default never alters an
34 // existing repository.46 // existing repository.
35 bool failing_checks_block = 2;47 bool failing_checks_block = 2;
48+ // Issues is the issue participation mode of the repository.
49+ IssuesMode issues = 3;
36 }50 }
37 51
38 // Repo is the metadata of a repository. Git-owned state (refs, trees, history)52 // Repo is the metadata of a repository. Git-owned state (refs, trees, history)
@@ -53,7 +67,7 @@ message Repo {
53 google.protobuf.Timestamp created_at = 5;67 google.protobuf.Timestamp created_at = 5;
54 // PushedAt is the time of the last push. It is unset until the first push.68 // PushedAt is the time of the last push. It is unset until the first push.
55 google.protobuf.Timestamp pushed_at = 6;69 google.protobuf.Timestamp pushed_at = 6;
56- // Settings are the merge settings of the repository. Always set in a70+ // Settings are the settings of the repository. Always set in a
57 // response, with the defaults for a repository that never changed them.71 // response, with the defaults for a repository that never changed them.
58 RepoSettings settings = 7;72 RepoSettings settings = 7;
59 // Topics classify the repository, as on GitHub: at most 20, each 1-5073 // Topics classify the repository, as on GitHub: at most 20, each 1-50
@@ -153,8 +167,9 @@ message UpdateRepoRequest {
153 string default_branch = 4;167 string default_branch = 4;
154 // UpdateMask lists the fields to change: "description", "visibility",168 // UpdateMask lists the fields to change: "description", "visibility",
155 // "default_branch", "topics", "settings.allowed_merge_modes" and/or169 // "default_branch", "topics", "settings.allowed_merge_modes" and/or
156- // "settings.failing_checks_block". "settings" stands for both settings170+ // "settings.failing_checks_block". "settings" stands for both merge settings
157- // paths. It must not be empty. Other paths are invalid_argument.171+ // paths. The optional issues field changes the issue mode separately.
172+ // It must not be empty. Other paths are invalid_argument.
158 google.protobuf.FieldMask update_mask = 5;173 google.protobuf.FieldMask update_mask = 5;
159 // Settings holds the new merge settings. Only the fields named in174 // Settings holds the new merge settings. Only the fields named in
160 // update_mask are used. allowed_merge_modes must then not be empty nor175 // update_mask are used. allowed_merge_modes must then not be empty nor
@@ -165,6 +180,8 @@ message UpdateRepoRequest {
165 // Topics replace the topics of the repository (see Repo.topics). Used only180 // Topics replace the topics of the repository (see Repo.topics). Used only
166 // if update_mask contains "topics"; an empty list then clears them.181 // if update_mask contains "topics"; an empty list then clears them.
167 repeated string topics = 7;182 repeated string topics = 7;
183+ // Issues is the new issue participation mode. When absent it is unchanged.
184+ optional IssuesMode issues = 8;
168 }185 }
169 186
170 // UpdateRepoResponse holds the repository after the update.187 // UpdateRepoResponse holds the repository after the update.
M proto/ermite/token.proto +3 -0
@@ -42,6 +42,9 @@ enum Scope {
4242 // SCOPE_MR_REVIEW allows setting a verdict (approve, changes requested) on
4343 // merge request notes. Wire name: mr:review.
4444 SCOPE_MR_REVIEW = 10;
45+ // SCOPE_ISSUE_WRITE allows creating, commenting on, closing and reopening
46+ // issues. Wire name: issue:write.
47+ SCOPE_ISSUE_WRITE = 11;
4548 }
4649
4750 // Token describes a stored token. It never contains the secret nor its hash.
@@ -42,6 +42,9 @@ enum Scope {
42 // SCOPE_MR_REVIEW allows setting a verdict (approve, changes requested) on42 // SCOPE_MR_REVIEW allows setting a verdict (approve, changes requested) on
43 // merge request notes. Wire name: mr:review.43 // merge request notes. Wire name: mr:review.
44 SCOPE_MR_REVIEW = 10;44 SCOPE_MR_REVIEW = 10;
45+ // SCOPE_ISSUE_WRITE allows creating, commenting on, closing and reopening
46+ // issues. Wire name: issue:write.
47+ SCOPE_ISSUE_WRITE = 11;
45 }48 }
46 49
47 // Token describes a stored token. It never contains the secret nor its hash.50 // Token describes a stored token. It never contains the secret nor its hash.

Keyboard shortcuts

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

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

Branches 2

Tags 18

↑↓ moveenter openesc close