Repository navigation
Description
Problem
When the per-worktree stack catalogs are migrated to the shared catalog, one conflict between two unrelated, old worktrees blocks every gh stack command in every worktree of the clone, including read-only ones like gh stack view --json:
migrating stack state: cannot migrate stack catalogs: stack identity has differing definitions; sources: [".../worktrees/<old-a>/gh-stack" ".../worktrees/<old-b>/gh-stack"] ...
In our case the two conflicting catalogs belonged to stale worktrees whose branches have nothing to do with the stack being viewed. Nobody working in the current worktree can proceed without editing other worktrees' catalogs.
Cause
Every command resolves stack state through MigrateLegacyState (
gh-stack/cmd/worktree_utils.go
Line 56 in 2a49e67
| if err := stack.MigrateLegacyState(commonDir); err != nil { |
gh-stack/cmd/worktree_utils.go
Line 274 in 2a49e67
| if err := stack.MigrateLegacyState(commonDir); err != nil { |
mergeMigrationCatalogs returns on the first stack-identity or shared-branch conflict and the whole migration aborts (gh-stack/internal/stack/migration.go
Lines 353 to 403 in 2a49e67
| func mergeMigrationCatalogs(commonDir string, catalogs []migrationCatalog) (*StackFile, error) { | |
| merged := &StackFile{SchemaVersion: schemaVersion, Stacks: []Stack{}} | |
| var sources []string | |
| repositorySource := "" | |
| for _, catalog := range catalogs { | |
| path := filepath.Join(commonDir, filepath.FromSlash(catalog.Path)) | |
| sf, err := parseMigrationCatalog(catalog) | |
| if err != nil { | |
| var conflict *MigrationConflictError | |
| if errors.As(err, &conflict) { | |
| conflict.Sources = []string{path} | |
| } | |
| return nil, fmt.Errorf("parsing migration catalog %q: %w", path, err) | |
| } | |
| if sf.Repository != "" { | |
| if merged.Repository != "" && merged.Repository != sf.Repository { | |
| return nil, migrationConflict("repository identities differ", []string{repositorySource, path}, nil) | |
| } | |
| merged.Repository = sf.Repository | |
| repositorySource = path | |
| } | |
| for _, s := range sf.Stacks { | |
| duplicate := false | |
| for i, other := range merged.Stacks { | |
| if equivalentMigrationStacks(s, other) { | |
| duplicate = true | |
| break | |
| } | |
| var shared []string | |
| for _, branch := range s.Branches { | |
| if other.IndexOf(branch.Branch) >= 0 { | |
| shared = append(shared, branch.Branch) | |
| } | |
| } | |
| sameIdentity := (s.ID != "" && s.ID == other.ID) || (s.Number != 0 && s.Number == other.Number) | |
| if sameIdentity { | |
| branches := append(s.BranchNames(), other.BranchNames()...) | |
| return nil, migrationConflict("stack identity has differing definitions", []string{sources[i], path}, branches) | |
| } | |
| if len(shared) > 0 { | |
| return nil, migrationConflict("non-trunk branches belong to differing stack definitions", []string{sources[i], path}, shared) | |
| } | |
| } | |
| if !duplicate { | |
| merged.Stacks = append(merged.Stacks, s) | |
| sources = append(sources, path) | |
| } | |
| } | |
| } | |
| return merged, nil | |
| } |
Related: #595 (same error after git fsck) and #610 (an unrelated duplicate worktree checkout blocks all commands), same pattern of one unrelated item failing the whole clone.
Expected
Migrate the stacks that merge cleanly and keep conflicting definitions out of the shared catalog (left in place, or quarantined), then fail only commands that touch the conflicting stacks or branches. A recovery command that lists and resolves the conflicts would replace hand-editing worktree internals.
This issue was drafted with AI assistance.
Posted on behalf of @schickling
| field | value |
|---|---|
agent_identity |
agent/dotfiles/upstream-manager |
session |
01a10cc8 |
agent_persona |
generalist |
agent_supervisor |
unavailable |
agent_tool |
OMP |
agent_tool_version |
18.12.5 |
agent_runtime |
OMP 18.12.5 |
agent_model |
anthropic/claude-opus-5-5 |
worktree |
unknown |
Activity