-
Notifications
You must be signed in to change notification settings - Fork 296
fix: detect reset-driven session divergence in status #948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,10 +18,17 @@ import ( | |
| "github.com/entireio/cli/cmd/entire/cli/settings" | ||
| "github.com/entireio/cli/cmd/entire/cli/strategy" | ||
| "github.com/entireio/cli/cmd/entire/cli/stringutil" | ||
| "github.com/entireio/cli/cmd/entire/cli/trailers" | ||
|
|
||
| "github.com/go-git/go-git/v6" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| type headLinkage struct { | ||
| commitHash string | ||
| checkpointID string | ||
| } | ||
|
|
||
| func newStatusCmd() *cobra.Command { | ||
| var detailed bool | ||
|
|
||
|
|
@@ -244,6 +251,12 @@ func writeActiveSessions(ctx context.Context, w io.Writer, sty statusStyles) { | |
| return | ||
| } | ||
|
|
||
| repoRoot, head, headErr := currentHeadLinkage(ctx) | ||
| divergenceWarnings := make(map[string]string) | ||
| if headErr == nil && repoRoot != "" && head.commitHash != "" { | ||
| divergenceWarnings = reconcileActiveSessionHeadDivergence(ctx, store, active, repoRoot, head) | ||
| } | ||
|
|
||
| // Group by worktree path | ||
| groups := make(map[string]*worktreeGroup) | ||
| for _, s := range active { | ||
|
|
@@ -342,6 +355,9 @@ func writeActiveSessions(ctx context.Context, w io.Writer, sty statusStyles) { | |
| } else { | ||
| fmt.Fprintln(w, sty.render(sty.dim, statsLine)) | ||
| } | ||
| if warning := divergenceWarnings[st.SessionID]; warning != "" { | ||
| fmt.Fprintf(w, "%s %s\n", sty.render(sty.yellow, "!"), sty.render(sty.yellow, warning)) | ||
| } | ||
| fmt.Fprintln(w) | ||
| } | ||
| } | ||
|
|
@@ -427,3 +443,77 @@ func resolveWorktreeBranchGit(ctx context.Context, worktreePath string) string { | |
| } | ||
| return detachedHEADDisplay | ||
| } | ||
|
|
||
| func currentHeadLinkage(ctx context.Context) (string, headLinkage, error) { | ||
| repoRoot, err := paths.WorktreeRoot(ctx) | ||
| if err != nil { | ||
| return "", headLinkage{}, fmt.Errorf("resolve worktree root: %w", err) | ||
| } | ||
|
|
||
| repo, err := git.PlainOpen(repoRoot) | ||
| if err != nil { | ||
| return "", headLinkage{}, fmt.Errorf("open repo: %w", err) | ||
| } | ||
|
|
||
| headRef, err := repo.Head() | ||
| if err != nil { | ||
| return "", headLinkage{}, fmt.Errorf("resolve HEAD: %w", err) | ||
| } | ||
|
|
||
| commit, err := repo.CommitObject(headRef.Hash()) | ||
| if err != nil { | ||
| return "", headLinkage{}, fmt.Errorf("load HEAD commit: %w", err) | ||
| } | ||
|
|
||
| head := headLinkage{commitHash: headRef.Hash().String()} | ||
| if checkpointIDs := trailers.ParseAllCheckpoints(commit.Message); len(checkpointIDs) > 0 { | ||
| head.checkpointID = checkpointIDs[0].String() | ||
| } | ||
|
Comment on lines
+468
to
+471
|
||
|
|
||
| return repoRoot, head, nil | ||
| } | ||
|
|
||
| func reconcileActiveSessionHeadDivergence( | ||
| ctx context.Context, | ||
| store *session.StateStore, | ||
| active []*session.State, | ||
| repoRoot string, | ||
| head headLinkage, | ||
| ) map[string]string { | ||
| warnings := make(map[string]string) | ||
| normalizedRepoRoot := normalizeWorktreePath(repoRoot) | ||
|
|
||
| for _, st := range active { | ||
| if normalizeWorktreePath(st.WorktreePath) != normalizedRepoRoot || st.BaseCommit == "" || st.BaseCommit == head.commitHash { | ||
| continue | ||
| } | ||
|
|
||
| if !st.LastCheckpointID.IsEmpty() && head.checkpointID != "" && st.LastCheckpointID.String() == head.checkpointID { | ||
| st.BaseCommit = head.commitHash | ||
| st.AttributionBaseCommit = head.commitHash | ||
| if err := store.Save(ctx, st); err != nil { | ||
| warnings[st.SessionID] = "tracking diverged from current HEAD; failed to refresh local linkage state" | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| if head.checkpointID != "" { | ||
| warnings[st.SessionID] = "tracking diverged from current HEAD; HEAD links to checkpoint " + head.checkpointID | ||
| continue | ||
| } | ||
|
|
||
| warnings[st.SessionID] = "tracking diverged from current HEAD after git history movement" | ||
| } | ||
|
|
||
| return warnings | ||
| } | ||
|
|
||
| func normalizeWorktreePath(path string) string { | ||
| if path == "" { | ||
| return "" | ||
| } | ||
| if resolved, err := filepath.EvalSymlinks(path); err == nil { | ||
| return filepath.Clean(resolved) | ||
| } | ||
| return filepath.Clean(path) | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currentHeadLinkage only records the first
Entire-Checkpointtrailer (checkpointIDs[0]).trailers.ParseAllCheckpointsexplicitly supports multiple checkpoint trailers (e.g., squash merges) while preserving order, so HEAD may legitimately link to several checkpoints. With the current implementation, a session whoseLastCheckpointIDmatches a later trailer will incorrectly fail to reconcile and will show a divergence warning. Consider storing all parsed checkpoint IDs (e.g., as a[]stringor amap[string]struct{}inheadLinkage) and treating HEAD as matching ifLastCheckpointIDequals any of them; update the warning text accordingly (e.g., mention the matched/available checkpoint IDs).