Something went wrong. Try again.
An alternative CLI for git-bug that simplifies working with agents on issues
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211package main
import ( "fmt"
"github.com/git-bug/git-bug/entities/bug" "github.com/git-bug/git-bug/entities/common" "github.com/git-bug/git-bug/entities/identity" "github.com/git-bug/git-bug/entity" "github.com/git-bug/git-bug/entity/dag" "github.com/git-bug/git-bug/repository")
// supersededByMetadataKey marks the comment that closes a bug replaced by a// clone. Its value is the full ID of the replacement bug.const supersededByMetadataKey = "superseded-by"
// supersededBy returns the full ID of the bug that replaced this one, or "".func supersededBy(snap *bug.Snapshot) string { for _, op := range snap.Operations { if _, ok := op.(*bug.AddCommentOperation); !ok { continue } if id, ok := op.GetMetadata(supersededByMetadataKey); ok && id != "" { return id } } return ""}
// shortID abbreviates a full ID for display.func shortID(fullID string) string { if len(fullID) > 7 { return fullID[:7] } return fullID}
// cloneBug re-creates a bug by replaying every operation of the original// with its original author and time, replacing the create-operation// metadata keys in overrides (e.g. parent, due). It is the only way to// change data git-bug stores immutably. The clone is committed; the// original is left untouched.func cloneBug(repo repository.ClockedRepo, original *bug.Bug, overrides map[string]string) (*bug.Bug, error) { snap := original.Compile() if len(snap.Operations) == 0 { return nil, fmt.Errorf("bug %s has no history to copy", shortID(original.Id().String())) } createID := snap.Operations[0].Id()
clone := bug.NewBug() // Old operation ID -> new operation ID, for ops that target other ops newIDs := make(map[entity.Id]entity.Id, len(snap.Operations)) remap := func(target entity.Id) (entity.Id, error) { id, ok := newIDs[target] if !ok { return "", fmt.Errorf("bug %s references an operation that cannot be copied", shortID(original.Id().String())) } return id, nil }
for _, op := range snap.Operations { author := op.Author() unixTime := op.Time().Unix()
var copied bug.Operation var ownMetadata map[string]string
switch o := op.(type) { case *bug.CreateOperation: c := bug.NewCreateOp(author, unixTime, o.Title, o.Message, o.Files) ownMetadata = withoutKeys(o.Metadata, overrides) for key, value := range overrides { ownMetadata[key] = value } copied = c case *bug.AddCommentOperation: copied = bug.NewAddCommentOp(author, unixTime, o.Message, o.Files) ownMetadata = o.Metadata case *bug.EditCommentOperation: target, err := remap(o.Target) if err != nil { return nil, err } copied = bug.NewEditCommentOp(author, unixTime, target, o.Message, o.Files) ownMetadata = o.Metadata case *bug.SetTitleOperation: copied = bug.NewSetTitleOp(author, unixTime, o.Title, o.Was) ownMetadata = o.Metadata case *bug.LabelChangeOperation: copied = bug.NewLabelChangeOperation(author, unixTime, o.Added, o.Removed) ownMetadata = o.Metadata case *bug.SetStatusOperation: copied = bug.NewSetStatusOp(author, unixTime, o.Status) ownMetadata = o.Metadata case *dag.SetMetadataOperation[*bug.Snapshot]: newMetadata := o.NewMetadata if o.Target == createID { // The clone carries overridden keys in the create operation newMetadata = withoutKeys(newMetadata, overrides) } if len(newMetadata) == 0 { continue } target, err := remap(o.Target) if err != nil { return nil, err } copied = bug.NewSetMetadataOp(author, unixTime, target, newMetadata) ownMetadata = o.Metadata case *dag.NoOpOperation[*bug.Snapshot]: copied = dag.NewNoOpOp[*bug.Snapshot](bug.NoOpOp, author, unixTime) ownMetadata = o.Metadata default: return nil, fmt.Errorf("bug %s contains a change of type %d that cannot be copied", shortID(original.Id().String()), op.Type()) }
for key, value := range ownMetadata { copied.SetMetadata(key, value) } if err := copied.Validate(); err != nil { return nil, fmt.Errorf("failed to copy bug %s: %w", shortID(original.Id().String()), err) } clone.Append(copied) newIDs[op.Id()] = copied.Id() }
if err := clone.Commit(repo); err != nil { return nil, fmt.Errorf("failed to save copy of bug %s: %w", shortID(original.Id().String()), err) } return clone, nil}
// withoutKeys returns a copy of m without the keys of drop (never nil).func withoutKeys(m map[string]string, drop map[string]string) map[string]string { out := make(map[string]string, len(m)) for k, v := range m { if _, ok := drop[k]; !ok { out[k] = v } } return out}
// supersedeBug closes the original bug with a note pointing at its clone.// reason completes "re-created there to change its ...".func supersedeBug(repo repository.ClockedRepo, original *bug.Bug, cloneFullID, reason string, author identity.Interface, unixTime int64) error { message := fmt.Sprintf("Superseded by %s: this bug was re-created there to change its %s. Continue work on %s.", shortID(cloneFullID), reason, shortID(cloneFullID)) metadata := map[string]string{supersededByMetadataKey: cloneFullID} if _, _, err := bug.AddComment(original, author, unixTime, message, nil, metadata); err != nil { return fmt.Errorf("failed to mark bug %s as superseded: %w", shortID(original.Id().String()), err) } if original.Compile().Status != common.ClosedStatus { if _, err := bug.Close(original, author, unixTime, nil); err != nil { return fmt.Errorf("failed to close superseded bug %s: %w", shortID(original.Id().String()), err) } } if err := original.Commit(repo); err != nil { return fmt.Errorf("failed to save superseded bug %s: %w", shortID(original.Id().String()), err) } return nil}
// recreateBug re-creates root with its create metadata overridden by// rootOverrides. Because create metadata is immutable, the bug and all// its live descendants are cloned (descendants re-linked under their// cloned parents), and every original is closed as superseded. Returns// old full ID -> new full ID in the order bugs were cloned (root first).func recreateBug(repo repository.ClockedRepo, root *bug.Bug, rootOverrides map[string]string, reason string, issues []BugIssue, author identity.Interface, unixTime int64) ([][2]string, error) { children := make(map[string][]string) for _, issue := range issues { if issue.ParentID != "" && issue.SupersededBy == "" { children[issue.ParentID] = append(children[issue.ParentID], issue.FullID) } }
var moved [][2]string var walk func(original *bug.Bug, overrides map[string]string) error walk = func(original *bug.Bug, overrides map[string]string) error { clone, err := cloneBug(repo, original, overrides) if err != nil { return err } oldID, newID := original.Id().String(), clone.Id().String() moved = append(moved, [2]string{oldID, newID})
for _, childID := range children[oldID] { child, err := bug.Read(repo, entity.Id(childID)) if err != nil { return fmt.Errorf("failed to read child bug %s: %w", shortID(childID), err) } if err := walk(child, map[string]string{parentMetadataKey: newID}); err != nil { return err } } // Supersede only after descendants are copied, so a failure part // way leaves the original subtree open and usable. childReason := reason if oldID != root.Id().String() { childReason = "parent" } return supersedeBug(repo, original, newID, childReason, author, unixTime) }
if err := walk(root, rootOverrides); err != nil { return moved, err } return moved, nil}