detached 7474f50

fix(cli): require resolve support before resolving issues

commit
7474f50f0c035a407b80f6ece604e179f3809336
author
brice · · 2026-10-22 18:36 UTC
committer
brice · · 2026-10-22 18:41 UTC
parent
74e7324
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +17 -1
M internal/app/cli_issue.go +3 -0
@@ -478,6 +478,9 @@ func newIssueCloseCmd(env *cliEnv) *cobra.Command {
478478 func newIssueResolveCmd(env *cliEnv) *cobra.Command {
479479 return newIssueStateCmd(env, "resolve", "resolved", "Resolve an issue", "Mark an open issue as resolved (fixed). `ermite issue reopen` opens it again. Needs the issue:write scope.",
480480 func(ctx context.Context, c *client.Client, repo string, n int64) (*ermite.Issue, error) {
481+ if err := requireFeature(ctx, c, "issues.resolve", "cannot resolve issues (issue resolve)"); err != nil {
482+ return nil, err
483+ }
481484 res, err := c.Issues().ResolveIssue(ctx, connect.NewRequest(&ermite.ResolveIssueRequest{Repo: repo, Number: n}))
482485 if err != nil {
483486 return nil, err
@@ -478,6 +478,9 @@ func newIssueCloseCmd(env *cliEnv) *cobra.Command {
478 func newIssueResolveCmd(env *cliEnv) *cobra.Command {478 func newIssueResolveCmd(env *cliEnv) *cobra.Command {
479 return newIssueStateCmd(env, "resolve", "resolved", "Resolve an issue", "Mark an open issue as resolved (fixed). `ermite issue reopen` opens it again. Needs the issue:write scope.",479 return newIssueStateCmd(env, "resolve", "resolved", "Resolve an issue", "Mark an open issue as resolved (fixed). `ermite issue reopen` opens it again. Needs the issue:write scope.",
480 func(ctx context.Context, c *client.Client, repo string, n int64) (*ermite.Issue, error) {480 func(ctx context.Context, c *client.Client, repo string, n int64) (*ermite.Issue, error) {
481+ if err := requireFeature(ctx, c, "issues.resolve", "cannot resolve issues (issue resolve)"); err != nil {
482+ return nil, err
483+ }
481 res, err := c.Issues().ResolveIssue(ctx, connect.NewRequest(&ermite.ResolveIssueRequest{Repo: repo, Number: n}))484 res, err := c.Issues().ResolveIssue(ctx, connect.NewRequest(&ermite.ResolveIssueRequest{Repo: repo, Number: n}))
482 if err != nil {485 if err != nil {
483 return nil, err486 return nil, err
M internal/app/cli_issue_test.go +14 -1
@@ -303,7 +303,9 @@ func TestIssueResolveAndHistory(t *testing.T) {
303303 {Id: "1", Kind: ermite.IssueEventKind_ISSUE_EVENT_KIND_REFERENCED, Issue: 2, MergeRequest: 5, MergeRequestTitle: "Fix", Closes: true, Actor: "owner", CreatedAt: ts("2026-10-01T10:00:00Z")},
304304 {Id: "2", Kind: ermite.IssueEventKind_ISSUE_EVENT_KIND_RESOLVED, Issue: 2, MergeRequest: 5, Actor: "merger", CreatedAt: ts("2026-10-03T10:00:00Z")},
305305 }
306- f := &fakeAPI{getVersion: withIssues,
306+ f := &fakeAPI{getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
307+ return &ermite.GetVersionResponse{Features: []string{"issues", "issues.resolve"}}, nil
308+ },
307309 resolveIssue: func(r *ermite.ResolveIssueRequest) (*ermite.ResolveIssueResponse, error) {
308310 assert.Equal(t, "alpha", r.GetRepo())
309311 assert.EqualValues(t, 2, r.GetNumber())
@@ -333,3 +335,14 @@ func TestIssueResolveAndHistory(t *testing.T) {
333335 assert.Contains(t, res.stdout, `"merge_request_title": "Fix"`)
334336 assert.Contains(t, res.stdout, `"kind": "resolved"`)
335337 }
338+
339+func TestIssueResolveRequiresFeature(t *testing.T) {
340+ f := &fakeAPI{getVersion: withIssues, resolveIssue: func(*ermite.ResolveIssueRequest) (*ermite.ResolveIssueResponse, error) {
341+ t.Error("ResolveIssue must not be called")
342+ return nil, errors.New("unexpected ResolveIssue")
343+ }}
344+ res := runCLI(t, envFor(f.serve(t)), "", "issue", "resolve", "alpha", "2")
345+ require.Error(t, res.err)
346+ assert.Contains(t, res.err.Error(), "the server does not support issues.resolve")
347+ assert.Contains(t, res.err.Error(), "cannot resolve issues")
348+}
@@ -303,7 +303,9 @@ func TestIssueResolveAndHistory(t *testing.T) {
303 {Id: "1", Kind: ermite.IssueEventKind_ISSUE_EVENT_KIND_REFERENCED, Issue: 2, MergeRequest: 5, MergeRequestTitle: "Fix", Closes: true, Actor: "owner", CreatedAt: ts("2026-10-01T10:00:00Z")},303 {Id: "1", Kind: ermite.IssueEventKind_ISSUE_EVENT_KIND_REFERENCED, Issue: 2, MergeRequest: 5, MergeRequestTitle: "Fix", Closes: true, Actor: "owner", CreatedAt: ts("2026-10-01T10:00:00Z")},
304 {Id: "2", Kind: ermite.IssueEventKind_ISSUE_EVENT_KIND_RESOLVED, Issue: 2, MergeRequest: 5, Actor: "merger", CreatedAt: ts("2026-10-03T10:00:00Z")},304 {Id: "2", Kind: ermite.IssueEventKind_ISSUE_EVENT_KIND_RESOLVED, Issue: 2, MergeRequest: 5, Actor: "merger", CreatedAt: ts("2026-10-03T10:00:00Z")},
305 }305 }
306- f := &fakeAPI{getVersion: withIssues,306+ f := &fakeAPI{getVersion: func(*ermite.GetVersionRequest) (*ermite.GetVersionResponse, error) {
307+ return &ermite.GetVersionResponse{Features: []string{"issues", "issues.resolve"}}, nil
308+ },
307 resolveIssue: func(r *ermite.ResolveIssueRequest) (*ermite.ResolveIssueResponse, error) {309 resolveIssue: func(r *ermite.ResolveIssueRequest) (*ermite.ResolveIssueResponse, error) {
308 assert.Equal(t, "alpha", r.GetRepo())310 assert.Equal(t, "alpha", r.GetRepo())
309 assert.EqualValues(t, 2, r.GetNumber())311 assert.EqualValues(t, 2, r.GetNumber())
@@ -333,3 +335,14 @@ func TestIssueResolveAndHistory(t *testing.T) {
333 assert.Contains(t, res.stdout, `"merge_request_title": "Fix"`)335 assert.Contains(t, res.stdout, `"merge_request_title": "Fix"`)
334 assert.Contains(t, res.stdout, `"kind": "resolved"`)336 assert.Contains(t, res.stdout, `"kind": "resolved"`)
335 }337 }
338+
339+func TestIssueResolveRequiresFeature(t *testing.T) {
340+ f := &fakeAPI{getVersion: withIssues, resolveIssue: func(*ermite.ResolveIssueRequest) (*ermite.ResolveIssueResponse, error) {
341+ t.Error("ResolveIssue must not be called")
342+ return nil, errors.New("unexpected ResolveIssue")
343+ }}
344+ res := runCLI(t, envFor(f.serve(t)), "", "issue", "resolve", "alpha", "2")
345+ require.Error(t, res.err)
346+ assert.Contains(t, res.err.Error(), "the server does not support issues.resolve")
347+ assert.Contains(t, res.err.Error(), "cannot resolve issues")
348+}

Keyboard shortcuts

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

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

Branches 4

Tags 45

↑↓ moveenter openesc close