Skip to content

Commit 1daa545

Browse files
authored
Merge pull request #2 from Martian-Engineering/feat/pb-update-parent
feat(pb): support --parent in update
2 parents 9909763 + 7ef3c61 commit 1daa545

4 files changed

Lines changed: 60 additions & 8 deletions

File tree

.pebbles/events.jsonl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,6 @@
292292
{"type":"create","timestamp":"2026-01-21T17:11:10.851111Z","issue_id":"pb-f42","payload":{"description":"Update CHANGELOG for 0.4.0, tag release, and run scripts/release.sh v0.4.0.","priority":"1","title":"Cut v0.4.0 release","type":"task"}}
293293
{"type":"status_update","timestamp":"2026-01-21T17:11:15.116199Z","issue_id":"pb-f42","payload":{"status":"in_progress"}}
294294
{"type":"close","timestamp":"2026-01-21T17:12:16.63905Z","issue_id":"pb-f42","payload":{}}
295+
{"type":"create","timestamp":"2026-01-23T22:04:09.803613Z","issue_id":"pb-533","payload":{"description":"Support --parent on pb update (set/clear parent), validate parent exists and not self, record event, reflect in model and show/list output, update help/docs.","priority":"2","title":"Add parent flag to pb update","type":"task"}}
296+
{"type":"status_update","timestamp":"2026-01-23T22:04:13.49696Z","issue_id":"pb-533","payload":{"status":"in_progress"}}
297+
{"type":"close","timestamp":"2026-01-24T01:44:50.295883Z","issue_id":"pb-533","payload":{}}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ pb version
7474

7575
# Update status and fields
7676
pb update pb-abc --status in_progress --type bug --priority P1 --description "Investigate regressions"
77+
# Set or clear a parent
78+
pb update pb-abc --parent pb-epic
79+
pb update pb-abc --parent none
7780

7881
# Close an issue
7982
pb close pb-abc

cmd/pb/help_text.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,20 +150,24 @@ Usage:
150150
pb update <id> --status in_progress
151151
pb update <id> --type bug --priority P1
152152
pb update --description "Updated scope" <id>
153+
pb update <id> --parent pb-epic
153154
154155
Flags:
155156
--status <status> New status (open, in_progress, closed). Example: --status in_progress
156157
--type <type> Replace issue type (free-form). Example: --type chore
157158
--description <text> Replace description (Markdown ok). Example: --description "New details"
158159
--priority <P0-P4> Replace priority (P0-P4 or 0-4). Example: --priority P0
160+
--parent <id|none> Replace parent issue. Example: --parent pb-epic
159161
160162
Details:
161163
- You can update multiple fields in one command.
162164
- Setting status to closed sets closed_at; other statuses clear closed_at.
165+
- Clear the parent with --parent none (or --parent "").
163166
164167
Workflows:
165168
- Start work: pb update <id> --status in_progress
166169
- Raise priority: pb update <id> --priority P1
170+
- Set parent: pb update <id> --parent pb-epic
167171
`
168172

169173
const closeHelp = `Close an issue.

cmd/pb/main.go

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,11 @@ func runUpdate(root string, args []string) {
328328
var issueType optionalString
329329
var description optionalString
330330
var priority optionalString
331+
var parent optionalString
331332
fs.Var(&issueType, "type", "New issue type")
332333
fs.Var(&description, "description", "New description")
333334
fs.Var(&priority, "priority", "New priority (P0-P4)")
335+
fs.Var(&parent, "parent", "Replace parent issue (use \"none\" to clear)")
334336
// Support `pb update <id> --status ...` by moving the id to the end.
335337
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
336338
args = append(args[1:], args[0])
@@ -343,7 +345,7 @@ func runUpdate(root string, args []string) {
343345
if fs.NArg() != 1 {
344346
exitError(fmt.Errorf("update requires issue id"))
345347
}
346-
if strings.TrimSpace(*status) == "" && !issueType.set && !description.set && !priority.set {
348+
if strings.TrimSpace(*status) == "" && !issueType.set && !description.set && !priority.set && !parent.set {
347349
exitError(fmt.Errorf("at least one field is required"))
348350
}
349351
if issueType.set && strings.TrimSpace(issueType.value) == "" {
@@ -354,16 +356,15 @@ func runUpdate(root string, args []string) {
354356
}
355357
id := fs.Arg(0)
356358
// Confirm the issue exists in the cache.
357-
if _, _, err := pebbles.GetIssue(root, id); err != nil {
359+
issue, _, err := pebbles.GetIssue(root, id)
360+
if err != nil {
358361
exitError(err)
359362
}
363+
id = issue.ID
360364
timestamp := pebbles.NowTimestamp()
365+
var events []pebbles.Event
361366
if strings.TrimSpace(*status) != "" {
362-
event := pebbles.NewStatusEvent(id, *status, timestamp)
363-
// Append the event and rebuild the cache for consistency.
364-
if err := pebbles.AppendEvent(root, event); err != nil {
365-
exitError(err)
366-
}
367+
events = append(events, pebbles.NewStatusEvent(id, *status, timestamp))
367368
}
368369
updatePayload := make(map[string]string)
369370
if issueType.set {
@@ -380,7 +381,41 @@ func runUpdate(root string, args []string) {
380381
updatePayload["priority"] = fmt.Sprintf("%d", parsed)
381382
}
382383
if len(updatePayload) > 0 {
383-
event := pebbles.NewUpdateEvent(id, timestamp, updatePayload)
384+
events = append(events, pebbles.NewUpdateEvent(id, timestamp, updatePayload))
385+
}
386+
if parent.set {
387+
trimmedParent := strings.TrimSpace(parent.value)
388+
clearParent := trimmedParent == "" || strings.EqualFold(trimmedParent, "none")
389+
var parentIssue pebbles.Issue
390+
if !clearParent {
391+
parentIssue, _, err = pebbles.GetIssue(root, trimmedParent)
392+
if err != nil {
393+
exitError(err)
394+
}
395+
if parentIssue.ID == issue.ID {
396+
exitError(fmt.Errorf("parent must be different from issue %s", issue.ID))
397+
}
398+
}
399+
hierarchy, err := pebbles.GetIssueHierarchy(root, issue.ID)
400+
if err != nil {
401+
exitError(err)
402+
}
403+
for _, existingParent := range issueIDsFromIssues(hierarchy.Parents) {
404+
events = append(events, pebbles.NewDepRemoveEvent(issue.ID, existingParent, pebbles.DepTypeParentChild, timestamp))
405+
}
406+
if !clearParent {
407+
childID := issue.ID
408+
if !pebbles.HasParentChildSuffix(parentIssue.ID, childID) {
409+
childID, err = pebbles.NextChildIssueID(root, parentIssue.ID)
410+
if err != nil {
411+
exitError(err)
412+
}
413+
events = append(events, pebbles.NewRenameEvent(issue.ID, childID, timestamp))
414+
}
415+
events = append(events, pebbles.NewDepAddEvent(childID, parentIssue.ID, pebbles.DepTypeParentChild, timestamp))
416+
}
417+
}
418+
for _, event := range events {
384419
if err := pebbles.AppendEvent(root, event); err != nil {
385420
exitError(err)
386421
}
@@ -959,6 +994,13 @@ func printIssue(root string, issue pebbles.Issue, hierarchy pebbles.IssueHierarc
959994
fmt.Println(header)
960995
// Core metadata block.
961996
fmt.Printf("Type: %s\n", renderIssueType(issue.IssueType))
997+
if len(hierarchy.Parents) > 0 {
998+
label := "Parent"
999+
if len(hierarchy.Parents) > 1 {
1000+
label = "Parents"
1001+
}
1002+
fmt.Printf("%s: %s\n", label, strings.Join(issueIDsFromIssues(hierarchy.Parents), ", "))
1003+
}
9621004
fmt.Printf(
9631005
"Created: %s · Updated: %s\n\n",
9641006
formatDate(issue.CreatedAt),

0 commit comments

Comments
 (0)