Skip to content

Commit 2542c14

Browse files
committed
Add --blocked-by flag to edit and support multiple IDs in link
The edit command now accepts --blocked-by to add dependencies inline, and both edit and link accept multiple IDs via comma separation or repeated flags (e.g. --blocked-by id1,id2 or --blocked-by id1 --blocked-by id2).
1 parent f37b3d1 commit 2542c14

5 files changed

Lines changed: 140 additions & 15 deletions

File tree

docs/epics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ The existing circular dependency check follows explicit `blocked_by` chains and
107107
| `bs close` | **Rejected** (derived) | Normal (triggers parent recomputation) |
108108
| `bs reopen` | **Rejected** (derived) | Normal (triggers parent recomputation) |
109109
| `bs edit --status` | **Rejected** (derived) | Normal (triggers parent recomputation) |
110-
| `bs edit` (other fields) | Allowed (title, description, priority, type, assignee, tags) | Normal |
110+
| `bs edit` (other fields) | Allowed (title, description, priority, type, assignee, tags, blocked-by) | Normal |
111111
| `bs comment` | Allowed | Allowed |
112112
| `bs link` / `bs unlink` | Allowed | Allowed |
113113
| `bs delete` | **Only if all children are closed or deleted** | Normal (triggers parent recomputation) |

internal/cli/commands.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ func newEditCmd() *cobra.Command {
147147
var assignee string
148148
var addTags []string
149149
var removeTags []string
150+
var blockedBy []string
150151

151152
cmd := &cobra.Command{
152153
Use: "edit <id>",
@@ -184,13 +185,26 @@ func newEditCmd() *cobra.Command {
184185
body["remove_tags"] = removeTags
185186
}
186187

187-
if len(body) == 0 {
188+
if len(body) == 0 && len(blockedBy) == 0 {
188189
return fmt.Errorf("no fields to update")
189190
}
190191

191-
data, err := c.Do("PATCH", "/api/v1/beads/"+args[0], body)
192-
if err != nil {
193-
return err
192+
var data []byte
193+
if len(body) > 0 {
194+
data, err = c.Do("PATCH", "/api/v1/beads/"+args[0], body)
195+
if err != nil {
196+
return err
197+
}
198+
}
199+
200+
for _, dep := range blockedBy {
201+
linkBody := map[string]any{
202+
"blocked_by": dep,
203+
}
204+
data, err = c.Do("POST", "/api/v1/beads/"+args[0]+"/link", linkBody)
205+
if err != nil {
206+
return err
207+
}
194208
}
195209

196210
out, err := prettyJSON(data)
@@ -210,6 +224,7 @@ func newEditCmd() *cobra.Command {
210224
cmd.Flags().StringVar(&assignee, "assignee", "", "assignee")
211225
cmd.Flags().StringSliceVar(&addTags, "add-tag", nil, "add a tag")
212226
cmd.Flags().StringSliceVar(&removeTags, "remove-tag", nil, "remove a tag")
227+
cmd.Flags().StringSliceVar(&blockedBy, "blocked-by", nil, "add dependency (ID of blocking bead, repeatable)")
213228

214229
return cmd
215230
}

internal/cli/commands_query.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,14 @@ func newCommentCmd() *cobra.Command {
215215
}
216216

217217
func newLinkCmd() *cobra.Command {
218-
var blockedBy string
218+
var blockedBy []string
219219

220220
cmd := &cobra.Command{
221221
Use: "link <id>",
222222
Short: "Add a dependency to a bead",
223223
Args: cobra.ExactArgs(1),
224224
RunE: func(cmd *cobra.Command, args []string) error {
225-
if blockedBy == "" {
225+
if len(blockedBy) == 0 {
226226
return fmt.Errorf("--blocked-by is required")
227227
}
228228

@@ -231,13 +231,15 @@ func newLinkCmd() *cobra.Command {
231231
return err
232232
}
233233

234-
body := map[string]any{
235-
"blocked_by": blockedBy,
236-
}
237-
238-
data, err := c.Do("POST", "/api/v1/beads/"+args[0]+"/link", body)
239-
if err != nil {
240-
return err
234+
var data []byte
235+
for _, dep := range blockedBy {
236+
body := map[string]any{
237+
"blocked_by": dep,
238+
}
239+
data, err = c.Do("POST", "/api/v1/beads/"+args[0]+"/link", body)
240+
if err != nil {
241+
return err
242+
}
241243
}
242244

243245
out, err := prettyJSON(data)
@@ -249,7 +251,7 @@ func newLinkCmd() *cobra.Command {
249251
},
250252
}
251253

252-
cmd.Flags().StringVar(&blockedBy, "blocked-by", "", "ID of the blocking bead")
254+
cmd.Flags().StringSliceVar(&blockedBy, "blocked-by", nil, "add dependency (ID of blocking bead, repeatable)")
253255

254256
return cmd
255257
}

internal/cli/commands_query_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,28 @@ func TestLinkUnlinkDeps(t *testing.T) {
361361
}
362362
}
363363

364+
func TestLink_MultipleBlockedBy(t *testing.T) {
365+
ts := startTestServer(t)
366+
setClientEnv(t, ts.URL)
367+
368+
out := runCmd(t, "add", "Blocker A")
369+
blockerA := parseBeadFromOutput(t, out)
370+
371+
out = runCmd(t, "add", "Blocker B")
372+
blockerB := parseBeadFromOutput(t, out)
373+
374+
out = runCmd(t, "add", "Target")
375+
target := parseBeadFromOutput(t, out)
376+
377+
// Comma-separated
378+
out = runCmd(t, "link", target.ID, "--blocked-by", blockerA.ID+","+blockerB.ID)
379+
linked := parseBeadFromOutput(t, out)
380+
381+
if len(linked.BlockedBy) != 2 {
382+
t.Fatalf("blocked_by = %v, want 2 entries", linked.BlockedBy)
383+
}
384+
}
385+
364386
func TestLink_MissingBlockedBy(t *testing.T) {
365387
ts := startTestServer(t)
366388
setClientEnv(t, ts.URL)

internal/cli/commands_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,92 @@ func TestEdit_NoFlags(t *testing.T) {
248248
}
249249
}
250250

251+
func TestEdit_BlockedBy(t *testing.T) {
252+
ts := startTestServer(t)
253+
setClientEnv(t, ts.URL)
254+
255+
// Create two beads
256+
out := runCmd(t, "add", "Blocker bead")
257+
blocker := parseBeadFromOutput(t, out)
258+
259+
out = runCmd(t, "add", "Blocked bead")
260+
blocked := parseBeadFromOutput(t, out)
261+
262+
// Use edit --blocked-by to add the dependency
263+
out = runCmd(t, "edit", blocked.ID, "--blocked-by", blocker.ID)
264+
edited := parseBeadFromOutput(t, out)
265+
266+
if len(edited.BlockedBy) != 1 || edited.BlockedBy[0] != blocker.ID {
267+
t.Errorf("blocked_by = %v, want [%s]", edited.BlockedBy, blocker.ID)
268+
}
269+
}
270+
271+
func TestEdit_BlockedByWithOtherFields(t *testing.T) {
272+
ts := startTestServer(t)
273+
setClientEnv(t, ts.URL)
274+
275+
out := runCmd(t, "add", "Blocker")
276+
blocker := parseBeadFromOutput(t, out)
277+
278+
out = runCmd(t, "add", "Target")
279+
target := parseBeadFromOutput(t, out)
280+
281+
// Edit title and add dependency in one command
282+
out = runCmd(t, "edit", target.ID, "--title", "Updated target", "--blocked-by", blocker.ID)
283+
edited := parseBeadFromOutput(t, out)
284+
285+
if edited.Title != "Updated target" {
286+
t.Errorf("title = %q, want %q", edited.Title, "Updated target")
287+
}
288+
if len(edited.BlockedBy) != 1 || edited.BlockedBy[0] != blocker.ID {
289+
t.Errorf("blocked_by = %v, want [%s]", edited.BlockedBy, blocker.ID)
290+
}
291+
}
292+
293+
func TestEdit_BlockedByMultiple(t *testing.T) {
294+
ts := startTestServer(t)
295+
setClientEnv(t, ts.URL)
296+
297+
out := runCmd(t, "add", "Blocker A")
298+
blockerA := parseBeadFromOutput(t, out)
299+
300+
out = runCmd(t, "add", "Blocker B")
301+
blockerB := parseBeadFromOutput(t, out)
302+
303+
out = runCmd(t, "add", "Target")
304+
target := parseBeadFromOutput(t, out)
305+
306+
// Comma-separated
307+
out = runCmd(t, "edit", target.ID, "--blocked-by", blockerA.ID+","+blockerB.ID)
308+
edited := parseBeadFromOutput(t, out)
309+
310+
if len(edited.BlockedBy) != 2 {
311+
t.Fatalf("blocked_by = %v, want 2 entries", edited.BlockedBy)
312+
}
313+
}
314+
315+
func TestEdit_BlockedByRepeatedFlag(t *testing.T) {
316+
ts := startTestServer(t)
317+
setClientEnv(t, ts.URL)
318+
319+
out := runCmd(t, "add", "Blocker A")
320+
blockerA := parseBeadFromOutput(t, out)
321+
322+
out = runCmd(t, "add", "Blocker B")
323+
blockerB := parseBeadFromOutput(t, out)
324+
325+
out = runCmd(t, "add", "Target")
326+
target := parseBeadFromOutput(t, out)
327+
328+
// Repeated --blocked-by flags
329+
out = runCmd(t, "edit", target.ID, "--blocked-by", blockerA.ID, "--blocked-by", blockerB.ID)
330+
edited := parseBeadFromOutput(t, out)
331+
332+
if len(edited.BlockedBy) != 2 {
333+
t.Fatalf("blocked_by = %v, want 2 entries", edited.BlockedBy)
334+
}
335+
}
336+
251337
func TestClean_RemovesClosedBeads(t *testing.T) {
252338
ts := startTestServer(t)
253339
setClientEnv(t, ts.URL)

0 commit comments

Comments
 (0)