feat(lanes,ui): per-lane git surface — Changes panel, VS Code-style diff, branch-aware Add lane, redesigned tooltips (#34, #35)
- Changes card beside lane detail with five modes: Files tree, overview, Review (whole diff, per-file collapse), single file, commit history. - LaneDiffView: unified / side-by-side with independent scroll on both axes and mirrored scrollLeft, expand/collapse context, intra-line emphasis. - Staged-only commit box; git actions (fetch/pull/push/stage/unstage/ discard/commit/switch/branch-create/branch-delete) guarded in git-ops.js. - Server: git-diff.js + git-ops.js behind GET /api/lanes/:id/git/* and POST /api/lanes/:id/git/:action; gitFacts gains upstream/ahead/behind. - Add lane: new/existing branch, current branch, worktree list, prefill from the git bar's "New lane here"; /api/run/cwds gains a repo kind. - GlobalTooltip upgrades every native title into a themed anchored bubble; chart/info popovers use theme tokens.
This commit is contained in:
+45
-3
@@ -30,7 +30,7 @@ const STATUS_FILE_CAP = 20;
|
||||
* these variables. This module's whole job is to run git safely against repos other
|
||||
* than the one enclosing the current working directory.
|
||||
*/
|
||||
async function git(cwd, args) {
|
||||
async function git(cwd, args, { timeout = 0 } = {}) {
|
||||
// Build a clean environment: copy process.env but scrub git hook variables
|
||||
// that could point to the outer repo's git directory or index.
|
||||
const env = { ...process.env };
|
||||
@@ -58,12 +58,16 @@ async function git(cwd, args) {
|
||||
cwd,
|
||||
env,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
// 0 = no limit, node's own default. A network command (fetch/pull/push)
|
||||
// passes one so an unreachable remote fails instead of pinning a request
|
||||
// open forever; GIT_TERMINAL_PROMPT=0 only covers the prompting case.
|
||||
timeout,
|
||||
});
|
||||
return { stdout: result.stdout, stderr: result.stderr };
|
||||
} catch (err) {
|
||||
const error = new Error(`git ${args[0]} failed`);
|
||||
error.code = err.code;
|
||||
error.git = { args, code: err.code, stderr: err.stderr };
|
||||
error.git = { args, code: err.code, stderr: err.stderr, stdout: err.stdout };
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -640,7 +644,45 @@ async function gitFacts(dir) {
|
||||
const { dirty, untracked, head, files } = await statusCounts(dir);
|
||||
const branch = (await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
||||
const subject = (await git(dir, ["log", "-1", "--format=%s"])).stdout.trim();
|
||||
return { branch, head, subject, dirty, untracked, files };
|
||||
return { branch, head, subject, dirty, untracked, files, ...(await trackingFacts(dir)) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Where this branch stands against its upstream: the upstream's name and how
|
||||
* many commits each side has that the other does not. `unpushedCount` already
|
||||
* counted one half of this, but only the destroy guard ever saw it — a lane
|
||||
* that is three commits ahead of origin looks identical to a pushed one in the
|
||||
* UI, which is the state most worth seeing before a machine is shut down.
|
||||
*
|
||||
* A branch with no upstream (never pushed) reports `upstream: null` and null
|
||||
* counts — not zeros, which would claim "in sync" about a comparison that was
|
||||
* never possible. Never throws: these are extra fields on a polled endpoint.
|
||||
*/
|
||||
async function trackingFacts(dir) {
|
||||
let hasRemote = false;
|
||||
try {
|
||||
hasRemote = !!(await git(dir, ["remote"])).stdout.trim();
|
||||
} catch {
|
||||
/* unreadable repo — the caller already has the facts that matter */
|
||||
}
|
||||
try {
|
||||
const upstream = (
|
||||
await git(dir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"])
|
||||
).stdout.trim();
|
||||
const counts = (
|
||||
await git(dir, ["rev-list", "--left-right", "--count", `${upstream}...HEAD`])
|
||||
).stdout.trim();
|
||||
const [behind, ahead] = counts.split(/\s+/).map((n) => Number(n));
|
||||
// Anything but the two numbers git documents reports as unknown, not as
|
||||
// NaN: the UI treats a falsy count as "nothing to push", and NaN is falsy,
|
||||
// so a malformed line would render a diverged branch as in sync.
|
||||
if (!Number.isFinite(ahead) || !Number.isFinite(behind)) {
|
||||
return { hasRemote, upstream, ahead: null, behind: null };
|
||||
}
|
||||
return { hasRemote, upstream, ahead, behind };
|
||||
} catch {
|
||||
return { hasRemote, upstream: null, ahead: null, behind: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user