diff --git a/.changes/unreleased/Added-20241201-205446.yaml b/.changes/unreleased/Added-20241201-205446.yaml new file mode 100644 index 00000000..9cc57cd2 --- /dev/null +++ b/.changes/unreleased/Added-20241201-205446.yaml @@ -0,0 +1,5 @@ +kind: Added +body: >- + submit: Include merged changes in navigation comments + when restacking and resubmitting changes based on them. +time: 2024-12-01T20:54:46.755566Z diff --git a/branch_create.go b/branch_create.go index 054838c2..fdfdc5ae 100644 --- a/branch_create.go +++ b/branch_create.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "github.com/charmbracelet/log" @@ -110,6 +111,11 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, view ui.Vi var ( baseHash git.Hash restackOntoNew []string // branches to restack onto the new branch + + // Downstack history for the new branch + // and for those restacked on top of it. + newMergedDownstack *[]json.RawMessage + restackedMergedDownstack *[]json.RawMessage ) if cmd.Below { if cmd.Target == trunk { @@ -128,6 +134,16 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, view ui.Vi restackOntoNew = append(restackOntoNew, cmd.Target) baseName = b.Base baseHash = b.BaseHash + + // If the branch is at the bottom of the stack + // and has a merged downstack history, + // transfer it to the new branch. + if len(b.MergedDownstack) > 0 { + newMergedDownstack = &b.MergedDownstack + restackedMergedDownstack = new([]json.RawMessage) + } + + // TODO: Maybe this transfer should take place at submit time? } else if cmd.Insert { // If inserting, above the target branch, // restack all its upstack branches on top of the new branch. @@ -207,9 +223,10 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, view ui.Vi // and rollback to the original branch and staged changes. branchTx := store.BeginBranchTx() if err := branchTx.Upsert(ctx, state.UpsertRequest{ - Name: cmd.Name, - Base: baseName, - BaseHash: baseHash, + Name: cmd.Name, + Base: baseName, + BaseHash: baseHash, + MergedDownstack: newMergedDownstack, }); err != nil { return fmt.Errorf("add branch %v with base %v: %w", cmd.Name, baseName, err) } @@ -220,8 +237,9 @@ func (cmd *branchCreateCmd) Run(ctx context.Context, log *log.Logger, view ui.Vi // // We'll run a restack command after this to update the state. if err := branchTx.Upsert(ctx, state.UpsertRequest{ - Name: branch, - Base: cmd.Name, + Name: branch, + Base: cmd.Name, + MergedDownstack: restackedMergedDownstack, }); err != nil { return fmt.Errorf("update base branch of %v: %w", branch, err) } diff --git a/doc/src/guide/cr.md b/doc/src/guide/cr.md index 14112f22..5cb966c4 100644 --- a/doc/src/guide/cr.md +++ b/doc/src/guide/cr.md @@ -79,6 +79,16 @@ and the position of the current branch in it. This behavior may be changed with the $$spice.submit.navigationComment$$ configuration key. + +!!! info "Stack history in navigation comments" + + + When possible, git-spice will remember CRs as they're merged into trunk, + and continue to list them in navigation comments of branches + based on those changes. + However, it is unable to do this following complex stack manipulation + operations. + ### Non-interactive submission Use the `--fill` flag (or `-c` since ) diff --git a/internal/forge/shamhub/cli.go b/internal/forge/shamhub/cli.go index 56983e2d..571e10af 100644 --- a/internal/forge/shamhub/cli.go +++ b/internal/forge/shamhub/cli.go @@ -245,8 +245,21 @@ func (c *Cmd) Run(ts *testscript.TestScript, neg bool, args []string) { give = changes case "comments": + want := func(*ChangeComment) bool { return true } if len(args) != 0 { - ts.Fatalf("usage: shamhub dump comments") + changeIDs := make(map[int]struct{}) + for _, arg := range args { + n, err := strconv.Atoi(arg) + if err != nil { + ts.Fatalf("invalid change number: %s", err) + } + changeIDs[n] = struct{}{} + } + + want = func(c *ChangeComment) bool { + _, ok := changeIDs[c.Change] + return ok + } } // Actual change comments have non-deterministic IDs. @@ -264,6 +277,10 @@ func (c *Cmd) Run(ts *testscript.TestScript, neg bool, args []string) { var comments []changeComment for _, c := range shamComments { + if !want(c) { + continue + } + comments = append(comments, changeComment{ Change: c.Change, Body: c.Body, diff --git a/internal/spice/branch.go b/internal/spice/branch.go index fe582a99..648025e1 100644 --- a/internal/spice/branch.go +++ b/internal/spice/branch.go @@ -76,6 +76,12 @@ type LookupBranchResponse struct { // Head is the commit at the head of the branch. Head git.Hash + + // MergedDownstack is a list of branches that were previously + // downstack from this branch and have since been merged into trunk. + // + // This is used to correctly display the history of the branch. + MergedDownstack []json.RawMessage } // DeletedBranchError is returned when a branch was deleted out of band. @@ -112,10 +118,11 @@ func (s *Service) LookupBranch(ctx context.Context, name string) (*LookupBranchR // !nil | !nil | Branch is not known to the repository if storeErr == nil && gitErr == nil { out := &LookupBranchResponse{ - Base: resp.Base, - BaseHash: resp.BaseHash, - UpstreamBranch: resp.UpstreamBranch, - Head: head, + Base: resp.Base, + BaseHash: resp.BaseHash, + UpstreamBranch: resp.UpstreamBranch, + Head: head, + MergedDownstack: resp.MergedDownstack, } if resp.ChangeMetadata != nil { @@ -340,6 +347,10 @@ type LoadBranchItem struct { // UpstreamBranch is the name under which this branch // was pushed to the upstream repository. UpstreamBranch string + + // MergedDownstack contains information about any branches, + // which this one was based on, that have already been merged into trunk. + MergedDownstack []json.RawMessage } // LoadBranches loads all tracked branches @@ -370,12 +381,13 @@ func (s *Service) LoadBranches(ctx context.Context) ([]LoadBranchItem, error) { } items = append(items, LoadBranchItem{ - Name: name, - Head: resp.Head, - Base: resp.Base, - BaseHash: resp.BaseHash, - UpstreamBranch: resp.UpstreamBranch, - Change: resp.Change, + Name: name, + Head: resp.Head, + Base: resp.Base, + BaseHash: resp.BaseHash, + UpstreamBranch: resp.UpstreamBranch, + Change: resp.Change, + MergedDownstack: resp.MergedDownstack, }) } diff --git a/internal/spice/onto.go b/internal/spice/onto.go index 2ef4def1..b450c8e2 100644 --- a/internal/spice/onto.go +++ b/internal/spice/onto.go @@ -2,6 +2,7 @@ package spice import ( "context" + "encoding/json" "fmt" "go.abhg.dev/gs/internal/git" @@ -18,6 +19,9 @@ type BranchOntoRequest struct { // Onto is the target branch to move the branch onto. // Onto may be the trunk branch. Onto string + + // MergedDownstack for [Branch], if any. + MergedDownstack *[]json.RawMessage } // BranchOnto moves the commits of a branch onto a different base branch, @@ -50,9 +54,10 @@ func (s *Service) BranchOnto(ctx context.Context, req *BranchOntoRequest) error branchTx := s.store.BeginBranchTx() if err := branchTx.Upsert(ctx, state.UpsertRequest{ - Name: req.Branch, - Base: req.Onto, - BaseHash: ontoHash, + Name: req.Branch, + Base: req.Onto, + BaseHash: ontoHash, + MergedDownstack: req.MergedDownstack, }); err != nil { return fmt.Errorf("set base of branch %s to %s: %w", req.Branch, req.Onto, err) } diff --git a/internal/spice/stack_edit.go b/internal/spice/stack_edit.go index 9bc52ee5..b2e3e21f 100644 --- a/internal/spice/stack_edit.go +++ b/internal/spice/stack_edit.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -44,17 +45,41 @@ func (s *Service) StackEdit(ctx context.Context, req *StackEditRequest) (*StackE must.NotContainf(req.Stack, s.store.Trunk(), "cannot edit trunk") must.NotBeBlankf(req.Editor, "editor is required") + // TODO: assert that req.Stack[0] has trunk as its base. + bottomName := req.Stack[0] + bottom, err := s.LookupBranch(ctx, req.Stack[0]) + if err != nil { + return nil, fmt.Errorf("look up lowest branch (%q): %w", req.Stack[0], err) + } + branches, err := editStackFile(req.Editor, req.Stack) if err != nil { return nil, err } - base := s.store.Trunk() - for _, branch := range branches { - if err := s.BranchOnto(ctx, &BranchOntoRequest{ + base := bottom.Base + for idx, branch := range branches { + req := BranchOntoRequest{ Branch: branch, Onto: base, - }); err != nil { + } + + if len(bottom.MergedDownstack) > 0 { + // If the bottom-most branch is changing, + // copy the merged downstack over to it. + if idx == 0 && branch != bottomName { + req.MergedDownstack = &bottom.MergedDownstack + } + + // Also in that case, make sure to clear it + // from the new position of the original bottom branch. + if idx > 0 && branch == bottomName { + var newHistory []json.RawMessage + req.MergedDownstack = &newHistory + } + } + + if err := s.BranchOnto(ctx, &req); err != nil { return nil, fmt.Errorf("branch %v onto %v: %w", branch, base, err) } base = branch diff --git a/internal/spice/state/branch.go b/internal/spice/state/branch.go index d24b5466..63e3e599 100644 --- a/internal/spice/state/branch.go +++ b/internal/spice/state/branch.go @@ -67,6 +67,8 @@ type branchState struct { Base branchStateBase `json:"base"` Upstream *branchUpstreamState `json:"upstream,omitempty"` Change *branchChangeState `json:"change,omitempty"` + + MergedDownstack []json.RawMessage `json:"merged,omitempty"` } // branchKey returns the path to the JSON file for the given branch @@ -98,6 +100,15 @@ type LookupResponse struct { // UpstreamBranch is the name of the upstream branch // or an empty string if the branch is not tracking an upstream branch. UpstreamBranch string + + // MergedDownstack holds information about branches + // that were previously downstack from this branch + // that have since been merged into trunk. + // + // MergedDownstack is in the order that the branches were merged. + // For example, if the stack was main -> A -> B -> C, + // where C is this branch, MergedDownstack will be [A, B]. + MergedDownstack []json.RawMessage } // LookupBranch returns information about a tracked branch. @@ -109,8 +120,9 @@ func (s *Store) LookupBranch(ctx context.Context, name string) (*LookupResponse, } res := &LookupResponse{ - Base: state.Base.Name, - BaseHash: git.Hash(state.Base.Hash), + Base: state.Base.Name, + BaseHash: git.Hash(state.Base.Hash), + MergedDownstack: state.MergedDownstack, } if change := state.Change; change != nil { @@ -220,6 +232,10 @@ type UpsertRequest struct { // UpstreamBranch is the name of the upstream branch to track. // Leave nil to leave it unchanged, or set to an empty string to clear it. UpstreamBranch *string + + // MergedDownstack is a list of branches that were previously + // downstack from this branch that have since been merged into trunk. + MergedDownstack *[]json.RawMessage } // Upsert adds or updates information about a branch. @@ -298,6 +314,10 @@ func (tx *BranchTx) Upsert(ctx context.Context, req UpsertRequest) error { } } + if req.MergedDownstack != nil { + state.MergedDownstack = *req.MergedDownstack + } + tx.states[req.Name] = state tx.sets[req.Name] = struct{}{} delete(tx.dels, req.Name) diff --git a/repo_sync.go b/repo_sync.go index b0cd2d6f..f994124c 100644 --- a/repo_sync.go +++ b/repo_sync.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/json" "errors" "fmt" + "maps" "runtime" "slices" "sync" @@ -11,8 +13,11 @@ import ( "github.com/charmbracelet/log" "go.abhg.dev/gs/internal/forge" "go.abhg.dev/gs/internal/git" + "go.abhg.dev/gs/internal/graph" + "go.abhg.dev/gs/internal/must" "go.abhg.dev/gs/internal/secret" "go.abhg.dev/gs/internal/spice" + "go.abhg.dev/gs/internal/spice/state" "go.abhg.dev/gs/internal/text" "go.abhg.dev/gs/internal/ui" ) @@ -217,7 +222,9 @@ func (cmd *repoSyncCmd) Run( } } else { // Supported forge. Check for merged CRs and upstream branches. - branchesToDelete, err = cmd.findForgeMergedBranches(ctx, log, repo, remoteRepo, candidates, view) + branchesToDelete, err = cmd.findForgeMergedBranches( + ctx, log, repo, store, svc, remoteRepo, candidates, view, + ) if err != nil { return fmt.Errorf("find merged CRs: %w", err) } @@ -265,12 +272,18 @@ func (cmd *repoSyncCmd) findForgeMergedBranches( ctx context.Context, log *log.Logger, repo *git.Repository, + store spice.Store, + svc *spice.Service, remoteRepo forge.Repository, knownBranches []spice.LoadBranchItem, view ui.View, ) ([]branchDeletion, error) { type submittedBranch struct { - Name string + Name string + + Base string + MergedDownstack []json.RawMessage + Change forge.ChangeID Merged bool @@ -281,6 +294,9 @@ func (cmd *repoSyncCmd) findForgeMergedBranches( type trackedBranch struct { Name string + Base string + MergedDownstack []json.RawMessage + Change forge.ChangeID Merged bool RemoteHeadSHA git.Hash @@ -314,9 +330,11 @@ func (cmd *repoSyncCmd) findForgeMergedBranches( if b.Change != nil { b := &submittedBranch{ - Name: b.Name, - Change: b.Change.ChangeID(), - UpstreamBranch: upstreamBranch, + Name: b.Name, + Base: b.Base, + Change: b.Change.ChangeID(), + UpstreamBranch: upstreamBranch, + MergedDownstack: b.MergedDownstack, } submittedBranches = append(submittedBranches, b) } else { @@ -325,8 +343,10 @@ func (cmd *repoSyncCmd) findForgeMergedBranches( // a remote tracking branch: // either $remote/$UpstreamBranch or $remote/$branch exists. b := &trackedBranch{ - Name: b.Name, - UpstreamBranch: upstreamBranch, + Name: b.Name, + Base: b.Base, + UpstreamBranch: upstreamBranch, + MergedDownstack: b.MergedDownstack, } trackedBranches = append(trackedBranches, b) } @@ -400,17 +420,28 @@ func (cmd *repoSyncCmd) findForgeMergedBranches( } wg.Wait() - var branchesToDelete []branchDeletion + type mergedBranch struct { + Name string + Base string + UpstreamBranch string + MergedDownstack []json.RawMessage + ChangeID forge.ChangeID + } + + mergedBranches := make(map[string]mergedBranch) // name -> branch for _, branch := range submittedBranches { if !branch.Merged { continue } log.Infof("%v: %v was merged", branch.Name, branch.Change) - branchesToDelete = append(branchesToDelete, branchDeletion{ - BranchName: branch.Name, - UpstreamName: branch.UpstreamBranch, - }) + mergedBranches[branch.Name] = mergedBranch{ + Name: branch.Name, + Base: branch.Base, + UpstreamBranch: branch.UpstreamBranch, + MergedDownstack: branch.MergedDownstack, + ChangeID: branch.Change, + } } for _, branch := range trackedBranches { @@ -418,12 +449,17 @@ func (cmd *repoSyncCmd) findForgeMergedBranches( continue } + merged := mergedBranch{ + Name: branch.Name, + Base: branch.Base, + UpstreamBranch: branch.UpstreamBranch, + MergedDownstack: branch.MergedDownstack, + ChangeID: branch.Change, + } + if branch.RemoteHeadSHA == branch.LocalHeadSHA { log.Infof("%v: %v was merged", branch.Name, branch.Change) - branchesToDelete = append(branchesToDelete, branchDeletion{ - BranchName: branch.Name, - UpstreamName: branch.UpstreamBranch, - }) + mergedBranches[branch.Name] = merged continue } @@ -449,13 +485,89 @@ func (cmd *repoSyncCmd) findForgeMergedBranches( } if shouldDelete { - branchesToDelete = append(branchesToDelete, branchDeletion{ - BranchName: branch.Name, - UpstreamName: branch.UpstreamBranch, - }) + mergedBranches[branch.Name] = merged } } + if len(mergedBranches) == 0 { + return nil, nil + } + + // Sort the merged branches in topological order: trunk to upstacks. + // This will be used to propagate merged branch information. + topoBranches := graph.Toposort(slices.Sorted(maps.Keys(mergedBranches)), + func(name string) (string, bool) { + base := mergedBranches[name].Base + // Ordering matters only if the base was also merged. + _, ok := mergedBranches[base] + return base, ok + }) + + // For each merged branch, bubble up merged downstacks + // to their direct upstacks. + // + // This is done in topological order (branches closer to trunk first) + // so that if two consecutive branches were merged, + // both changes are bubbled up. + mergedDownstacks := make(map[string][]json.RawMessage) + for _, name := range topoBranches { + branch, ok := mergedBranches[name] + must.Bef(ok, "topologically sorted branch %q must be merged", name) + + aboves, err := svc.ListAbove(ctx, name) + if err != nil { + log.Warn("Unable to query merged branch's upstacks. Not propagating to merge history.", + "branch", name, "error", err) + continue + } + + changeIDJSON, err := remoteRepo.Forge().MarshalChangeID(branch.ChangeID) + if err != nil { + log.Warn("Unable to serialize ChangeID for merged branch. Not propagating to merge history.", + "branch", name, "changeID", branch.ChangeID, "error", err) + continue + } + + for _, above := range aboves { + // MergedDownstack for the upstack of the branch being merged + // is the branch's own merged downstack and the branch itself. + var newHistory []json.RawMessage + newHistory = append(newHistory, mergedDownstacks[name]...) + newHistory = append(newHistory, changeIDJSON) + // Combine with anything else already in the merged downstack. + newHistory = append(newHistory, mergedDownstacks[above]...) + mergedDownstacks[above] = newHistory + } + } + + // mergedDownstacks now contains the final merged downstack list + // for each of the upstack branches. Commit this information. + branchTx := store.BeginBranchTx() + for branch, history := range mergedDownstacks { + if _, ok := mergedBranches[branch]; ok { + history = nil // nuke history for merged branches + } + err := branchTx.Upsert(ctx, state.UpsertRequest{ + Name: branch, + MergedDownstack: &history, + }) + if err != nil { + log.Warnf("%v: unable to update merged downstacks: %v", branch, err) + } + delete(mergedDownstacks, branch) + } + if err := branchTx.Commit(ctx, "sync: propagate merged branches"); err != nil { + log.Warn("Unable to propagated merged downstacks", "error", err) + } + + branchesToDelete := make([]branchDeletion, 0, len(mergedBranches)) + for _, branch := range mergedBranches { + branchesToDelete = append(branchesToDelete, branchDeletion{ + BranchName: branch.Name, + UpstreamName: branch.UpstreamBranch, + }) + } + return branchesToDelete, nil } diff --git a/submit.go b/submit.go index c0d6de67..0f011aba 100644 --- a/submit.go +++ b/submit.go @@ -174,6 +174,7 @@ func updateNavigationComments( idxByBranch := make(map[string]int) // branch -> index in nodes // First pass: add nodes but don't connect. + f := remoteRepo.Forge() for _, b := range trackedBranches { if b.Change == nil { continue @@ -190,23 +191,62 @@ func updateNavigationComments( }) } - // Second pass: connect Aboves. + // Second pass: + // + // - Add merged downstacks as separate nodes. + // - Connect Aboves if this is a base to another node. for _, b := range trackedBranches { nodeIdx, ok := idxByBranch[b.Name] if !ok { continue } - baseIdx, ok := idxByBranch[b.Base] - if !ok { - continue + // Add nodes starting at the bottom. + // For each merged downstack branch: + // + // - previous branch is the base (starting at trunk) + // - current branch is added to Aboves of previous branch + lastDownstackIdx := -1 + for _, crJSON := range b.MergedDownstack { + downstackCR, err := f.UnmarshalChangeID(crJSON) + if err != nil { + log.Warn("skiping invalid downstack change", + "branch", b.Name, + "change", string(crJSON), + "error", err, + ) + continue + } + + idx := len(nodes) + nodes = append(nodes, &stackedChange{ + Change: downstackCR, + Base: lastDownstackIdx, + }) + // Inform previous node about this node. + if lastDownstackIdx != -1 { + nodes[lastDownstackIdx].Aboves = append(nodes[lastDownstackIdx].Aboves, idx) + } + lastDownstackIdx = idx } - node := nodes[nodeIdx] - node.Base = baseIdx + // If this branch's base is known, it'll be in idxByBranch. + // Otherwise it's trunk (-1) or a merged downstack branch, + // in which case we'll use the last of those. + baseIdx := lastDownstackIdx + if idx, ok := idxByBranch[b.Base]; ok { + // Tracked base always takes precedence. + baseIdx = idx + } - base := nodes[baseIdx] - base.Aboves = append(base.Aboves, nodeIdx) + // If the base is a known node, connect it in both directions. + if baseIdx != -1 { + node := nodes[nodeIdx] + node.Base = baseIdx + + base := nodes[baseIdx] + base.Aboves = append(base.Aboves, nodeIdx) + } } type ( diff --git a/testdata/script/README.md b/testdata/script/README.md index 848e910d..adeb88d9 100644 --- a/testdata/script/README.md +++ b/testdata/script/README.md @@ -232,10 +232,11 @@ Closes the CR without merging. shamhub dump changes shamhub dump change shamhub dump comments +shamhub dump comments [num] ... ``` -Dumps information about all changes, a single change, or all comments -to stdout. +Dumps information about all changes, a single change, +all comments, or comments for specific changes, respectively. #### shamhub register diff --git a/testdata/script/branch_create_below_with_downstack_history.txt b/testdata/script/branch_create_below_with_downstack_history.txt new file mode 100644 index 00000000..4ac2630c --- /dev/null +++ b/testdata/script/branch_create_below_with_downstack_history.txt @@ -0,0 +1,127 @@ +# 'branch create --below' on the bottom-most branch of a stack +# with a merged downstack history transfers the history +# to the new bottom-most branch. + +as 'Test ' +at '2024-12-20T09:39:40Z' + +# set up +cd repo +git init +git commit --allow-empty -m 'Initial commit' +gs repo init + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +# set up main -> feat1 -> feat2 -> feat3 +git add feat1.txt +gs branch create feat1 -m 'Add feature 1' +git add feat2.txt +gs branch create feat2 -m 'Add feature 2' +git add feat3.txt +gs branch create feat3 -m 'Add feature 3' +gs ss --fill + +gs ls +cmp stderr $WORK/golden/ls-before.txt + +# Merge feat1 to give feat2 a merge history +shamhub merge alice/example 1 +gs rs +gs sr +gs ss +shamhub dump comments 2 3 +cmp stdout $WORK/golden/feat1-merged-comments.txt + +# Introduce feat4 below feat2 +gs bottom +git add feat4.txt +gs bc --below feat4 -m 'Add feature 4' +gs ss --fill + +gs ls +cmp stderr $WORK/golden/ls-after.txt + +shamhub dump comments 2 3 4 +cmp stdout $WORK/golden/final-comments.txt + +-- repo/feat1.txt -- +feat 1 +-- repo/feat2.txt -- +feat 2 +-- repo/feat3.txt -- +feat 3 +-- repo/feat4.txt -- +feat 4 +-- golden/ls-before.txt -- + ┏━■ feat3 (#3) ◀ + ┏━┻□ feat2 (#2) +┏━┻□ feat1 (#1) +main +-- golden/ls-after.txt -- + ┏━□ feat3 (#3) + ┏━┻□ feat2 (#2) +┏━┻■ feat4 (#4) ◀ +main +-- golden/feat1-merged-comments.txt -- +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #2 ◀ + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +-- golden/final-comments.txt -- +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #4 + - #2 ◀ + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #4 + - #2 + - #3 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 4 + body: | + This change is part of the following stack: + + - #1 + - #4 ◀ + - #2 + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + diff --git a/testdata/script/branch_onto_two_stacks_with_downstack_history.txt b/testdata/script/branch_onto_two_stacks_with_downstack_history.txt new file mode 100644 index 00000000..0cea91ca --- /dev/null +++ b/testdata/script/branch_onto_two_stacks_with_downstack_history.txt @@ -0,0 +1,255 @@ +# Tests how navigation comments behave +# when a branch with a merged downstack +# is restacked onto another branch with its own stack. + +as 'Test ' +at '2024-06-22T12:24:34Z' + +# set up +cd repo +git init +git commit --allow-empty -m 'Initial commit' +gs repo init + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +git add feature1.txt +gs branch create feature1 -m 'Add feature 1' + +git add feature2.txt +gs branch create feature2 -m 'Add feature 2' + +git add feature3.txt +gs branch create feature3 -m 'Add feature 3' + +# Now we have: +# main -> feature1 -> feature2 -> feature3 +exists feature1.txt feature2.txt feature3.txt +gs ls -a +cmp stderr $WORK/golden/ls-before-stack1.txt + +gs ss --fill # stack submit + +shamhub dump comments +cmp stdout $WORK/golden/submit-first-stack-comments.txt + +gs bco main +git add stack2feature1.txt +gs branch create stack2feature1 -m 'Add stack 2 feature 1' + +git add stack2feature2.txt +gs branch create stack2feature2 -m 'Add stack 2 feature 2' + +# Now we have: +# main -> feature1 -> feature2 -> feature3 +# main -> stack2feature1 -> stack2feature2 +exists stack2feature1.txt stack2feature2.txt +gs ls -a +cmp stderr $WORK/golden/ls-before-stack2.txt + +gs ss --fill # stack submit + +shamhub dump comments 4 5 +cmp stdout $WORK/golden/submit-second-stack-comments.txt + +# Merge the bottom PRs (feature1 and stack2feature1), +# sync everything, restack, and submit. +gs trunk +shamhub merge alice/example 1 +shamhub merge alice/example 4 +gs rs +stderr '#1 was merged' +stderr '#4 was merged' +gs sr # stack restack +gs ss --fill # stack submit + +shamhub dump comments 2 3 5 +cmp stdout $WORK/golden/bottom-prs-merged-comments.txt + +# transpose stack2feature2 onto feature2, turning it into: +# main -> feature2 -> {feature3, stack2feature2} +gs bco stack2feature2 +gs branch onto feature2 +stderr 'stack2feature2: moved onto feature2' + +# restack everything and submit +gs bco feature2 +gs sr +gs ss + +# sanity check of the stack state +gs ls -a +cmp stderr $WORK/golden/ls-after.txt +gs bco stack2feature2 +exists feature1.txt feature2.txt stack2feature1.txt stack2feature2.txt + +# verify comments on the PRs +shamhub dump comments 2 3 5 +cmp stdout $WORK/golden/branch-onto-comments.txt + +-- repo/feature1.txt -- +Feature 1 +-- repo/feature2.txt -- +Feature 2 +-- repo/feature3.txt -- +Feature 3 +-- repo/feature4.txt -- +Feature 4 +-- repo/stack2feature1.txt -- +Stack 2 feature 1 +-- repo/stack2feature2.txt -- +Stack 2 feature 2 + +-- edit/give.txt -- +feature2 +feature3 +feature4 + +-- edit/want.txt -- +feature4 +feature3 +feature2 + +# Edit the order of branches by modifying the list above. +# The branch at the bottom of the list will be merged into trunk first. +# Branches above that will be stacked on top of it in the order they appear. +# Branches deleted from the list will not be modified. +# +# Save and quit the editor to apply the changes. +# Delete all lines in the editor to abort the operation. +-- golden/ls-before-stack1.txt -- + ┏━■ feature3 ◀ + ┏━┻□ feature2 +┏━┻□ feature1 +main +-- golden/ls-before-stack2.txt -- + ┏━□ feature3 (#3) + ┏━┻□ feature2 (#2) +┏━┻□ feature1 (#1) +┃ ┏━■ stack2feature2 ◀ +┣━┻□ stack2feature1 +main +-- golden/ls-after.txt -- + ┏━□ feature3 (#3) + ┣━□ stack2feature2 (#5) +┏━┻■ feature2 (#2) ◀ +main +-- golden/submit-first-stack-comments.txt -- +- change: 1 + body: | + This change is part of the following stack: + + - #1 ◀ + - #2 + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #2 ◀ + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +-- golden/submit-second-stack-comments.txt -- +- change: 4 + body: | + This change is part of the following stack: + + - #4 ◀ + - #5 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 5 + body: | + This change is part of the following stack: + + - #4 + - #5 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +-- golden/bottom-prs-merged-comments.txt -- +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #2 ◀ + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 5 + body: | + This change is part of the following stack: + + - #4 + - #5 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +-- golden/branch-onto-comments.txt -- +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #2 ◀ + - #3 + - #5 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 5 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #5 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + diff --git a/testdata/script/stack_edit_inserted_at_bottom_with_downstack_history.txt b/testdata/script/stack_edit_inserted_at_bottom_with_downstack_history.txt new file mode 100644 index 00000000..c79faea3 --- /dev/null +++ b/testdata/script/stack_edit_inserted_at_bottom_with_downstack_history.txt @@ -0,0 +1,189 @@ +# 'stack edit' a stack after submitting with a downstack history. + +as 'Test ' +at '2024-06-22T12:24:34Z' + +# set up +cd repo +git init +git commit --allow-empty -m 'Initial commit' +gs repo init + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +git add feature1.txt +gs branch create feature1 -m 'Add feature 1' + +git add feature2.txt +gs branch create feature2 -m 'Add feature 2' + +git add feature3.txt +gs branch create feature3 -m 'Add feature 3' + +git add feature4.txt +gs branch create feature4 -m 'Add feature 4' + +# Now we have: +# main -> feature1 -> feature2 -> feature3 -> feature4 +exists feature1.txt feature2.txt feature3.txt feature4.txt +gs ls -a +cmp stderr $WORK/golden/ls-before.txt + +gs ss --fill # stack submit + +shamhub dump comments +cmp stdout $WORK/golden/initial-comments.txt + +# Merge the bottom PR, sync, restack, and submit. +shamhub merge alice/example 1 +gs rs +stderr '#1 was merged' +gs sr # stack restack +gs ss # stack submit + +# Edit and resubmit the stack. +env MOCKEDIT_GIVE=$WORK/edit/give.txt MOCKEDIT_RECORD=$WORK/edit/got.txt +gs bco main +gs stack edit +cmp $WORK/edit/got.txt $WORK/edit/want.txt +gs sr +gs ss + +gs ls -a +cmp stderr $WORK/golden/ls-after.txt + +gs bco feature2 +exists feature1.txt feature2.txt feature3.txt feature4.txt + +shamhub dump comments 2 3 4 +cmp stdout $WORK/golden/post-edit-comments.txt + +-- repo/feature1.txt -- +Feature 1 +-- repo/feature2.txt -- +Feature 2 +-- repo/feature3.txt -- +Feature 3 +-- repo/feature4.txt -- +Feature 4 + +-- edit/give.txt -- +feature2 +feature3 +feature4 + +-- edit/want.txt -- +feature4 +feature3 +feature2 + +# Edit the order of branches by modifying the list above. +# The branch at the bottom of the list will be merged into trunk first. +# Branches above that will be stacked on top of it in the order they appear. +# Branches deleted from the list will not be modified. +# +# Save and quit the editor to apply the changes. +# Delete all lines in the editor to abort the operation. +-- golden/ls-before.txt -- + ┏━■ feature4 ◀ + ┏━┻□ feature3 + ┏━┻□ feature2 +┏━┻□ feature1 +main +-- golden/ls-after.txt -- + ┏━□ feature2 (#2) + ┏━┻□ feature3 (#3) +┏━┻□ feature4 (#4) +main ◀ +-- golden/initial-comments.txt -- +- change: 1 + body: | + This change is part of the following stack: + + - #1 ◀ + - #2 + - #3 + - #4 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #2 ◀ + - #3 + - #4 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 ◀ + - #4 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 4 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 + - #4 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +-- golden/ls.txt -- + ┏━□ feature3 (#3) + ┏━┻□ feature2 (#2) +┏━┻■ feature1 (#1) ◀ +main + +-- golden/post-edit-comments.txt -- +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #4 + - #3 + - #2 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #4 + - #3 ◀ + - #2 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 4 + body: | + This change is part of the following stack: + + - #1 + - #4 ◀ + - #3 + - #2 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + diff --git a/testdata/script/stack_submit.txt b/testdata/script/stack_submit.txt index 9a2061e9..c57d0369 100644 --- a/testdata/script/stack_submit.txt +++ b/testdata/script/stack_submit.txt @@ -211,8 +211,9 @@ INF Created #3: $SHAMHUB_URL/alice/example/change/3 body: | This change is part of the following stack: - - #2 ◀ - - #3 + - #1 + - #2 ◀ + - #3 Change managed by [git-spice](https://abhinav.github.io/git-spice/). @@ -220,8 +221,9 @@ INF Created #3: $SHAMHUB_URL/alice/example/change/3 body: | This change is part of the following stack: - - #2 - - #3 ◀ + - #1 + - #2 + - #3 ◀ Change managed by [git-spice](https://abhinav.github.io/git-spice/). diff --git a/testdata/script/stack_submit_multiple_merged_history.txt b/testdata/script/stack_submit_multiple_merged_history.txt new file mode 100644 index 00000000..ba40d9c6 --- /dev/null +++ b/testdata/script/stack_submit_multiple_merged_history.txt @@ -0,0 +1,125 @@ +# submit a stack of PRs with 'stack submit'. + +as 'Test ' +at '2024-04-05T16:40:32Z' + +# setup +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +# set up a fake GitHub remote +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +# create a stack: +# main -> feature1 -> feature2 -> feature3 +git add feature1.txt +gs branch create feature1 -m 'Add feature 1' +git add feature2.txt +gs branch create feature2 -m 'Add feature 2' +git add feature3.txt +gs branch create feature3 -m 'Add feature 3' + +env SHAMHUB_USERNAME=alice +gs auth login + +# submit the entire stack from the middle. +git checkout feature1 +gs stack submit --fill + +gs ls -a +cmp stderr $WORK/golden/ls.txt + +shamhub dump comments +cmp stdout $WORK/golden/start-comments.txt + +# Merge the bottom two PRs, sync, restack, and submit. +shamhub merge alice/example 1 +shamhub merge alice/example 2 +gs rs +stderr '#1 was merged' +stderr '#2 was merged' +gs sr # stack restack +gs ss # stack submit +stderr 'Updated #3' + +shamhub dump comments +cmp stdout $WORK/golden/post-merge-comments.txt + +-- repo/feature1.txt -- +This is feature 1 +-- repo/feature2.txt -- +This is feature 2 +-- repo/feature3.txt -- +This is feature 3 + +-- golden/start-comments.txt -- +- change: 1 + body: | + This change is part of the following stack: + + - #1 ◀ + - #2 + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #2 ◀ + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +-- golden/post-merge-comments.txt -- +- change: 1 + body: | + This change is part of the following stack: + + - #1 ◀ + - #2 + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 2 + body: | + This change is part of the following stack: + + - #1 + - #2 ◀ + - #3 + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +- change: 3 + body: | + This change is part of the following stack: + + - #1 + - #2 + - #3 ◀ + + Change managed by [git-spice](https://abhinav.github.io/git-spice/). + +-- golden/ls.txt -- + ┏━□ feature3 (#3) + ┏━┻□ feature2 (#2) +┏━┻■ feature1 (#1) ◀ +main diff --git a/testdata/script/stack_submit_update_leave_draft.txt b/testdata/script/stack_submit_update_leave_draft.txt index df1c05a9..5f08a8ad 100644 --- a/testdata/script/stack_submit_update_leave_draft.txt +++ b/testdata/script/stack_submit_update_leave_draft.txt @@ -224,8 +224,9 @@ INF CR #3 is up-to-date: $SHAMHUB_URL/alice/example/change/3 body: | This change is part of the following stack: - - #2 ◀ - - #3 + - #1 + - #2 ◀ + - #3 Change managed by [git-spice](https://abhinav.github.io/git-spice/). @@ -233,8 +234,9 @@ INF CR #3 is up-to-date: $SHAMHUB_URL/alice/example/change/3 body: | This change is part of the following stack: - - #2 - - #3 ◀ + - #1 + - #2 + - #3 ◀ Change managed by [git-spice](https://abhinav.github.io/git-spice/).