package main import ( "fmt" "github.com/git-bug/git-bug/entities/bug" ) // parentMetadataKey is the CreateOp metadata key holding the full ID // of the parent bug. Absent means "no parent". // // git-bug metadata keys are immutable once set: a later SetMetadata // cannot overwrite or delete the key. A parent link is added in place to // a bug that has none; changing an existing link re-creates the bug (see // applyCreateMetadata). Links cannot be removed. const parentMetadataKey = "parent" // currentParentID returns the bug's current parent full ID, or "". func currentParentID(snap *bug.Snapshot) string { if parent, ok := snap.GetCreateMetadata(parentMetadataKey); ok && parent != "" { return parent } return "" } // createsParentCycle reports whether linking bugFullID under parentFullID // would close a dependency cycle (i.e. bugFullID is already an ancestor // of parentFullID). Self-links are also reported as cycles. func createsParentCycle(issues []BugIssue, bugFullID, parentFullID string) bool { if bugFullID == parentFullID { return true } byID := make(map[string]BugIssue, len(issues)) for _, issue := range issues { byID[issue.FullID] = issue } for current := parentFullID; current != ""; { if current == bugFullID { return true } ancestor, ok := byID[current] if !ok { return false } current = ancestor.ParentID } return false } // resolveNewParent validates a requested parent for bug b and returns the // parent's full ID together with all bugs (for cycle and subtree checks). // An empty parentID returns "" and no bugs. func resolveNewParent(repoPath string, b *bug.Bug, parentID string) (string, []BugIssue, error) { if parentID == "" { return "", nil, nil } parentBug, err := resolveBugID(repoPath, parentID) if err != nil { return "", nil, fmt.Errorf("failed to resolve parent bug ID %q: %w\n\nUse 'bug ls' to see available bugs", parentID, err) } parentFullID := parentBug.Id().String() bugFullID := b.Id().String() if parentFullID == bugFullID { return "", nil, fmt.Errorf("bug cannot be its own parent") } if replacement := supersededBy(b.Compile()); replacement != "" { return "", nil, fmt.Errorf("bug %s was superseded by %s; edit %s instead", shortID(bugFullID), shortID(replacement), shortID(replacement)) } if replacement := supersededBy(parentBug.Compile()); replacement != "" { return "", nil, fmt.Errorf("parent %s was superseded by %s; use %s as the parent instead", shortID(parentFullID), shortID(replacement), shortID(replacement)) } issues, err := LoadBugs(repoPath) if err != nil { return "", nil, fmt.Errorf("failed to load bugs: %w", err) } if createsParentCycle(issues, bugFullID, parentFullID) { return "", nil, fmt.Errorf("setting parent %q would create a dependency cycle", parentID) } return parentFullID, issues, nil }