Skip to content

Commit 0d5e31f

Browse files
committed
Add delete and edit commands for managing individual notes
1 parent 2210385 commit 0d5e31f

8 files changed

Lines changed: 216 additions & 5 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ glab-discussion resolve DISCUSSION_ID
7777
glab-discussion resolve DISCUSSION_ID --unresolve
7878
```
7979

80+
### edit
81+
82+
Edit an existing note's body.
83+
84+
```bash
85+
glab-discussion edit NOTE_ID --body "Updated text"
86+
echo "From stdin" | glab-discussion edit NOTE_ID --body -
87+
```
88+
89+
### delete
90+
91+
Delete a note.
92+
93+
```bash
94+
glab-discussion delete NOTE_ID
95+
```
96+
8097
## Requirements
8198

8299
- [`glab` CLI](https://docs.gitlab.com/cli/) installed and authenticated

coding-agent-plugins/claude-code/skills/glab-discussion/SKILL.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ name: glab-discussion
33
description: >-
44
This skill should be used when the user asks to "read MR discussions",
55
"list MR comments", "reply to a discussion", "resolve a discussion",
6-
"add a diff note", "review MR comments", "dump discussions",
7-
"show MR diff for commenting", or needs to interact with GitLab merge request
8-
discussions. Provides CLI reference for the glab-discussion tool.
6+
"edit a comment", "delete a comment", "add a diff note", "review MR comments",
7+
"dump discussions", "show MR diff for commenting", or needs to interact with
8+
GitLab merge request discussions. Provides CLI reference for the glab-discussion tool.
99
---
1010

1111
# glab-discussion CLI
@@ -93,6 +93,19 @@ glab-discussion resolve <discussion_id>
9393
glab-discussion resolve <discussion_id> --unresolve
9494
```
9595

96+
### edit — Edit an existing note
97+
98+
```bash
99+
glab-discussion edit <note_id> --body "Updated text"
100+
glab-discussion edit <note_id> --body - # read body from stdin
101+
```
102+
103+
### delete — Delete a note
104+
105+
```bash
106+
glab-discussion delete <note_id>
107+
```
108+
96109
## Resolving GitLab UI URLs
97110

98111
GitLab UI links to specific notes use `#note_<id>` anchors (e.g.
@@ -113,6 +126,8 @@ and also in the filename suffix.
113126
3. **Check diff context:** `glab-discussion diff --file <path>` to see commentable lines
114127
4. **Reply:** `glab-discussion write --reply-to <id> --body "..."`
115128
5. **Add diff note:** `glab-discussion write --file <path> --new-line <n> --body "..."`
116-
6. **Resolve:** `glab-discussion resolve <id>`
117-
7. **Re-read:** `glab-discussion read` to see updated state (incremental, only changed files)
129+
6. **Edit a note:** `glab-discussion edit <note_id> --body "..."`
130+
7. **Delete a note:** `glab-discussion delete <note_id>`
131+
8. **Resolve:** `glab-discussion resolve <id>`
132+
9. **Re-read:** `glab-discussion read` to see updated state (incremental, only changed files)
118133

src/glab_discussion/cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ def main(argv: list[str] | None = None) -> None:
4343
resolve_parser.add_argument("discussion_id", help="Discussion ID to resolve")
4444
resolve_parser.add_argument("--unresolve", action="store_true", default=False, help="Unresolve instead of resolve")
4545

46+
# --- delete ---
47+
delete_parser = subparsers.add_parser("delete", parents=[mr_parent], help="Delete a note")
48+
delete_parser.add_argument("note_id", type=int, help="Note ID to delete")
49+
50+
# --- edit ---
51+
edit_parser = subparsers.add_parser("edit", parents=[mr_parent], help="Edit a note")
52+
edit_parser.add_argument("note_id", type=int, help="Note ID to edit")
53+
edit_parser.add_argument("--body", required=True, help='New note body text (use "-" for stdin)')
54+
4655
args = parser.parse_args(argv)
4756

4857
if args.command == "read":
@@ -60,6 +69,14 @@ def main(argv: list[str] | None = None) -> None:
6069
elif args.command == "resolve":
6170
from glab_discussion.commands.resolve import run
6271

72+
run(args)
73+
elif args.command == "delete":
74+
from glab_discussion.commands.delete import run
75+
76+
run(args)
77+
elif args.command == "edit":
78+
from glab_discussion.commands.edit import run
79+
6380
run(args)
6481
else:
6582
parser.print_help()
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
5+
from glab_discussion.api import glab_api
6+
from glab_discussion.context import resolve_mr_context
7+
8+
9+
def run(args: argparse.Namespace) -> None:
10+
ctx = resolve_mr_context(args)
11+
12+
glab_api(
13+
f"projects/{ctx.project_id}/merge_requests/{ctx.mr_iid}/notes/{args.note_id}",
14+
method="DELETE",
15+
hostname=ctx.hostname,
16+
)
17+
18+
print(f"Deleted note {args.note_id}")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import sys
5+
6+
from glab_discussion.api import glab_api
7+
from glab_discussion.context import resolve_mr_context
8+
9+
10+
def run(args: argparse.Namespace) -> None:
11+
ctx = resolve_mr_context(args)
12+
13+
body = sys.stdin.read() if args.body == "-" else args.body
14+
15+
glab_api(
16+
f"projects/{ctx.project_id}/merge_requests/{ctx.mr_iid}/notes/{args.note_id}",
17+
method="PUT",
18+
raw_fields={"body": body},
19+
hostname=ctx.hostname,
20+
)
21+
22+
print(f"Edited note {args.note_id}")

tests/test_cli.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,31 @@ def test_resolve_help(self, capsys) -> None:
4343
assert "discussion_id" in captured.out
4444
assert "--unresolve" in captured.out
4545

46+
def test_delete_help(self, capsys) -> None:
47+
with pytest.raises(SystemExit) as exc_info:
48+
main(["delete", "--help"])
49+
assert exc_info.value.code == 0
50+
captured = capsys.readouterr()
51+
assert "note_id" in captured.out
52+
53+
def test_edit_help(self, capsys) -> None:
54+
with pytest.raises(SystemExit) as exc_info:
55+
main(["edit", "--help"])
56+
assert exc_info.value.code == 0
57+
captured = capsys.readouterr()
58+
assert "note_id" in captured.out
59+
assert "--body" in captured.out
60+
61+
def test_delete_requires_note_id(self) -> None:
62+
with pytest.raises(SystemExit) as exc_info:
63+
main(["delete"])
64+
assert exc_info.value.code == 2
65+
66+
def test_edit_requires_body(self) -> None:
67+
with pytest.raises(SystemExit) as exc_info:
68+
main(["edit", "123"])
69+
assert exc_info.value.code == 2
70+
4671
def test_invalid_command(self) -> None:
4772
with pytest.raises(SystemExit) as exc_info:
4873
main(["nonexistent"])

tests/test_delete.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from argparse import Namespace
4+
from unittest.mock import patch
5+
6+
from glab_discussion.commands.delete import run
7+
from glab_discussion.models import MrContext
8+
9+
10+
class TestDelete:
11+
def test_delete_note(self, capsys) -> None:
12+
ctx = MrContext(
13+
hostname="gitlab.com",
14+
project_id=42,
15+
project_path="group/project",
16+
mr_iid=7,
17+
mr_url="https://gitlab.com/group/project/-/merge_requests/7",
18+
)
19+
20+
with (
21+
patch("glab_discussion.commands.delete.resolve_mr_context", return_value=ctx),
22+
patch("glab_discussion.commands.delete.glab_api") as mock_api,
23+
):
24+
args = Namespace(note_id=99999)
25+
run(args)
26+
27+
mock_api.assert_called_once_with(
28+
"projects/42/merge_requests/7/notes/99999",
29+
method="DELETE",
30+
hostname="gitlab.com",
31+
)
32+
33+
captured = capsys.readouterr()
34+
assert "Deleted note 99999" in captured.out

tests/test_edit.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
from argparse import Namespace
4+
from unittest.mock import patch
5+
6+
from glab_discussion.commands.edit import run
7+
from glab_discussion.models import MrContext
8+
9+
10+
class TestEdit:
11+
def test_edit_note(self, capsys) -> None:
12+
ctx = MrContext(
13+
hostname="gitlab.com",
14+
project_id=42,
15+
project_path="group/project",
16+
mr_iid=7,
17+
mr_url="https://gitlab.com/group/project/-/merge_requests/7",
18+
)
19+
20+
with (
21+
patch("glab_discussion.commands.edit.resolve_mr_context", return_value=ctx),
22+
patch("glab_discussion.commands.edit.glab_api") as mock_api,
23+
):
24+
args = Namespace(note_id=99999, body="Updated text")
25+
run(args)
26+
27+
mock_api.assert_called_once_with(
28+
"projects/42/merge_requests/7/notes/99999",
29+
method="PUT",
30+
raw_fields={"body": "Updated text"},
31+
hostname="gitlab.com",
32+
)
33+
34+
captured = capsys.readouterr()
35+
assert "Edited note 99999" in captured.out
36+
37+
def test_edit_note_stdin(self, capsys) -> None:
38+
ctx = MrContext(
39+
hostname="gitlab.com",
40+
project_id=42,
41+
project_path="group/project",
42+
mr_iid=7,
43+
mr_url="https://gitlab.com/group/project/-/merge_requests/7",
44+
)
45+
46+
with (
47+
patch("glab_discussion.commands.edit.resolve_mr_context", return_value=ctx),
48+
patch("glab_discussion.commands.edit.glab_api") as mock_api,
49+
patch("glab_discussion.commands.edit.sys") as mock_sys,
50+
):
51+
mock_sys.stdin.read.return_value = "Body from stdin"
52+
args = Namespace(note_id=88888, body="-")
53+
run(args)
54+
55+
mock_api.assert_called_once_with(
56+
"projects/42/merge_requests/7/notes/88888",
57+
method="PUT",
58+
raw_fields={"body": "Body from stdin"},
59+
hostname="gitlab.com",
60+
)
61+
62+
captured = capsys.readouterr()
63+
assert "Edited note 88888" in captured.out

0 commit comments

Comments
 (0)