taskplane 0.30.4 → 0.30.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,3 +86,77 @@ export function runGitWithEnv(
86
86
  };
87
87
  }
88
88
  }
89
+
90
+ /**
91
+ * Truthful description of an orch branch relative to its base, for completion
92
+ * messaging. Never claims "merged" unless the branch is verifiably ahead;
93
+ * a failed comparison is `unknown`, not "nothing to integrate".
94
+ *
95
+ * - `ahead` : N commit(s) ahead of base (integration has something to take)
96
+ * - `empty` : branch exists but is not ahead of base (nothing merged)
97
+ * - `missing` : branch does not exist
98
+ * - `unknown` : git comparison failed (detail carries the error)
99
+ */
100
+ export function describeOrchBranchState(
101
+ orchBranch: string,
102
+ baseBranch: string,
103
+ repoRoot: string,
104
+ ): { kind: "ahead" | "empty" | "missing" | "unknown"; aheadBy: number | null; detail: string } {
105
+ if (!orchBranch) return { kind: "missing", aheadBy: null, detail: "no orch branch recorded" };
106
+ const exists = runGit(["rev-parse", "--verify", "--quiet", `refs/heads/${orchBranch}`], repoRoot);
107
+ if (!exists.ok)
108
+ return { kind: "missing", aheadBy: null, detail: `branch ${orchBranch} does not exist` };
109
+ if (!baseBranch) return { kind: "unknown", aheadBy: null, detail: "no base branch recorded" };
110
+ const count = runGit(["rev-list", "--count", `${baseBranch}..${orchBranch}`], repoRoot);
111
+ if (!count.ok) {
112
+ return {
113
+ kind: "unknown",
114
+ aheadBy: null,
115
+ detail: `could not compare ${orchBranch} to ${baseBranch} (${(count.stderr || "git error").trim().slice(0, 120)})`,
116
+ };
117
+ }
118
+ const n = Number.parseInt(count.stdout.trim(), 10);
119
+ if (!Number.isFinite(n))
120
+ return { kind: "unknown", aheadBy: null, detail: "unparseable rev-list output" };
121
+ return n > 0
122
+ ? { kind: "ahead", aheadBy: n, detail: `${n} commit(s) ahead of ${baseBranch}` }
123
+ : { kind: "empty", aheadBy: 0, detail: `not ahead of ${baseBranch} (nothing was merged)` };
124
+ }
125
+
126
+ /**
127
+ * Aggregate `describeOrchBranchState` across every repo root a batch touched
128
+ * (workspace mode: primary + member repos). A batch whose only successful work
129
+ * landed in a secondary repo must not read "nothing to integrate" because the
130
+ * primary orch branch is empty (Sage review).
131
+ */
132
+ export function describeOrchBranchStateAcrossRepos(
133
+ orchBranch: string,
134
+ baseBranch: string,
135
+ repoRoots: Iterable<string>,
136
+ ): { kind: "ahead" | "empty" | "missing" | "unknown"; detail: string } {
137
+ const per: Array<{ root: string; state: ReturnType<typeof describeOrchBranchState> }> = [];
138
+ for (const root of new Set(repoRoots))
139
+ per.push({ root, state: describeOrchBranchState(orchBranch, baseBranch, root) });
140
+ if (per.length === 0) return { kind: "missing", detail: "no repo roots" };
141
+ if (per.length === 1) return { kind: per[0].state.kind, detail: per[0].state.detail };
142
+ const ahead = per.filter((p) => p.state.kind === "ahead");
143
+ const unknown = per.filter((p) => p.state.kind === "unknown");
144
+ const label = (root: string) => root.split(/[\/]/).filter(Boolean).pop() ?? root;
145
+ if (ahead.length > 0) {
146
+ return {
147
+ kind: "ahead",
148
+ detail:
149
+ ahead.map((p) => `${label(p.root)}: ${p.state.detail}`).join("; ") +
150
+ (unknown.length > 0 ? `; ${unknown.length} repo(s) unverifiable` : ""),
151
+ };
152
+ }
153
+ if (unknown.length > 0)
154
+ return {
155
+ kind: "unknown",
156
+ detail: unknown.map((p) => `${label(p.root)}: ${p.state.detail}`).join("; "),
157
+ };
158
+ return {
159
+ kind: "empty",
160
+ detail: `not ahead of ${baseBranch} in any of ${per.length} repos (nothing was merged)`,
161
+ };
162
+ }