feat: add --parent flag to bd update for reparenting issues (bd-cj2e)
Allows reparenting issues to a different epic/parent: bd update bd-xyz --parent=bd-epic Implementation: - Add Parent field to UpdateArgs in protocol.go - Handle reparenting in RPC handler (server_issues_epics.go) - Add --parent flag to updateCmd with both daemon and direct mode support - Remove old parent-child dependency before adding new one - Pass empty string to remove parent entirely 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -635,6 +635,10 @@ var updateCmd = &cobra.Command{
|
|||||||
setLabels, _ := cmd.Flags().GetStringSlice("set-labels")
|
setLabels, _ := cmd.Flags().GetStringSlice("set-labels")
|
||||||
updates["set_labels"] = setLabels
|
updates["set_labels"] = setLabels
|
||||||
}
|
}
|
||||||
|
if cmd.Flags().Changed("parent") {
|
||||||
|
parent, _ := cmd.Flags().GetString("parent")
|
||||||
|
updates["parent"] = parent
|
||||||
|
}
|
||||||
if cmd.Flags().Changed("type") {
|
if cmd.Flags().Changed("type") {
|
||||||
issueType, _ := cmd.Flags().GetString("type")
|
issueType, _ := cmd.Flags().GetString("type")
|
||||||
// Validate issue type
|
// Validate issue type
|
||||||
@@ -726,6 +730,9 @@ var updateCmd = &cobra.Command{
|
|||||||
if issueType, ok := updates["issue_type"].(string); ok {
|
if issueType, ok := updates["issue_type"].(string); ok {
|
||||||
updateArgs.IssueType = &issueType
|
updateArgs.IssueType = &issueType
|
||||||
}
|
}
|
||||||
|
if parent, ok := updates["parent"].(string); ok {
|
||||||
|
updateArgs.Parent = &parent
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := daemonClient.Update(updateArgs)
|
resp, err := daemonClient.Update(updateArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -771,7 +778,7 @@ var updateCmd = &cobra.Command{
|
|||||||
// Apply regular field updates if any
|
// Apply regular field updates if any
|
||||||
regularUpdates := make(map[string]interface{})
|
regularUpdates := make(map[string]interface{})
|
||||||
for k, v := range updates {
|
for k, v := range updates {
|
||||||
if k != "add_labels" && k != "remove_labels" && k != "set_labels" {
|
if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" {
|
||||||
regularUpdates[k] = v
|
regularUpdates[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -800,6 +807,50 @@ var updateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle parent reparenting (bd-cj2e)
|
||||||
|
if newParent, ok := updates["parent"].(string); ok {
|
||||||
|
// Validate new parent exists (unless empty string to remove parent)
|
||||||
|
if newParent != "" {
|
||||||
|
parentIssue, err := store.GetIssue(ctx, newParent)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error getting parent %s: %v\n", newParent, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if parentIssue == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: parent issue %s not found\n", newParent)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and remove existing parent-child dependency
|
||||||
|
deps, err := store.GetDependencyRecords(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error getting dependencies for %s: %v\n", id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, dep := range deps {
|
||||||
|
if dep.Type == types.DepParentChild {
|
||||||
|
if err := store.RemoveDependency(ctx, id, dep.DependsOnID, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error removing old parent dependency: %v\n", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new parent-child dependency (if not removing parent)
|
||||||
|
if newParent != "" {
|
||||||
|
newDep := &types.Dependency{
|
||||||
|
IssueID: id,
|
||||||
|
DependsOnID: newParent,
|
||||||
|
Type: types.DepParentChild,
|
||||||
|
}
|
||||||
|
if err := store.AddDependency(ctx, newDep, actor); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error adding parent dependency: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run update hook (bd-kwro.8)
|
// Run update hook (bd-kwro.8)
|
||||||
updatedIssue, _ := store.GetIssue(ctx, id)
|
updatedIssue, _ := store.GetIssue(ctx, id)
|
||||||
if updatedIssue != nil && hookRunner != nil {
|
if updatedIssue != nil && hookRunner != nil {
|
||||||
@@ -1496,6 +1547,7 @@ func init() {
|
|||||||
updateCmd.Flags().StringSlice("add-label", nil, "Add labels (repeatable)")
|
updateCmd.Flags().StringSlice("add-label", nil, "Add labels (repeatable)")
|
||||||
updateCmd.Flags().StringSlice("remove-label", nil, "Remove labels (repeatable)")
|
updateCmd.Flags().StringSlice("remove-label", nil, "Remove labels (repeatable)")
|
||||||
updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)")
|
updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)")
|
||||||
|
updateCmd.Flags().String("parent", "", "New parent issue ID (reparents the issue, use empty string to remove parent)")
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
editCmd.Flags().Bool("title", false, "Edit the title")
|
editCmd.Flags().Bool("title", false, "Edit the title")
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ type UpdateArgs struct {
|
|||||||
SupersededBy *string `json:"superseded_by,omitempty"` // Replacement issue ID if obsolete
|
SupersededBy *string `json:"superseded_by,omitempty"` // Replacement issue ID if obsolete
|
||||||
// Pinned field (bd-iea)
|
// Pinned field (bd-iea)
|
||||||
Pinned *bool `json:"pinned,omitempty"` // If true, issue is a persistent context marker
|
Pinned *bool `json:"pinned,omitempty"` // If true, issue is a persistent context marker
|
||||||
|
// Reparenting field (bd-cj2e)
|
||||||
|
Parent *string `json:"parent,omitempty"` // New parent issue ID (reparents the issue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseArgs represents arguments for the close operation
|
// CloseArgs represents arguments for the close operation
|
||||||
|
|||||||
@@ -466,8 +466,65 @@ func (s *Server) handleUpdate(req *Request) Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit mutation event for event-driven daemon (only if any updates or label operations were performed)
|
// Handle reparenting (bd-cj2e)
|
||||||
if len(updates) > 0 || len(updateArgs.SetLabels) > 0 || len(updateArgs.AddLabels) > 0 || len(updateArgs.RemoveLabels) > 0 {
|
if updateArgs.Parent != nil {
|
||||||
|
newParentID := *updateArgs.Parent
|
||||||
|
|
||||||
|
// Validate new parent exists (unless empty string to remove parent)
|
||||||
|
if newParentID != "" {
|
||||||
|
newParent, err := store.GetIssue(ctx, newParentID)
|
||||||
|
if err != nil {
|
||||||
|
return Response{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to get new parent: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if newParent == nil {
|
||||||
|
return Response{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("parent issue %s not found", newParentID),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and remove existing parent-child dependency
|
||||||
|
deps, err := store.GetDependencyRecords(ctx, updateArgs.ID)
|
||||||
|
if err != nil {
|
||||||
|
return Response{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to get dependencies: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, dep := range deps {
|
||||||
|
if dep.Type == types.DepParentChild {
|
||||||
|
if err := store.RemoveDependency(ctx, updateArgs.ID, dep.DependsOnID, actor); err != nil {
|
||||||
|
return Response{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to remove old parent dependency: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break // Only one parent-child dependency expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new parent-child dependency (if not removing parent)
|
||||||
|
if newParentID != "" {
|
||||||
|
newDep := &types.Dependency{
|
||||||
|
IssueID: updateArgs.ID,
|
||||||
|
DependsOnID: newParentID,
|
||||||
|
Type: types.DepParentChild,
|
||||||
|
}
|
||||||
|
if err := store.AddDependency(ctx, newDep, actor); err != nil {
|
||||||
|
return Response{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("failed to add parent dependency: %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit mutation event for event-driven daemon (only if any updates or label/parent operations were performed)
|
||||||
|
if len(updates) > 0 || len(updateArgs.SetLabels) > 0 || len(updateArgs.AddLabels) > 0 || len(updateArgs.RemoveLabels) > 0 || updateArgs.Parent != nil {
|
||||||
// Check if this was a status change - emit rich MutationStatus event
|
// Check if this was a status change - emit rich MutationStatus event
|
||||||
if updateArgs.Status != nil && *updateArgs.Status != string(issue.Status) {
|
if updateArgs.Status != nil && *updateArgs.Status != string(issue.Status) {
|
||||||
s.emitRichMutation(MutationEvent{
|
s.emitRichMutation(MutationEvent{
|
||||||
|
|||||||
Reference in New Issue
Block a user