fix(lanes,git): harden the lane git surface and fix client races in the Changes panel
Server (git-diff.js, git-ops.js, worktree.js, routes/lanes.js): - Every lane git call runs with GIT_LITERAL_PATHSPECS=1 and -z porcelain, so a file name is a file name, never a glob, and odd names parse correctly. - A lane that is a subdirectory of its repo sees and acts on ONLY its own paths (reads, stage/unstage/discard/commit, and gitFacts.files). - Any file named settings.local.json (the lane's router API key) is refused as 403 ESECRET on disk (symlinks resolved) and at every committed rev. - commit refuses with 400 ESTAGEDOUTSIDE while anything outside the lane is staged (checked with --no-renames, so a move into the lane is caught). - push resolves its remote (upstream → only remote → origin), else 400 ENOREMOTE; unstaging a rename unstages both halves. - Git writes serialize on the repo's git dir, not the lane id, so two lanes on one index cannot collide on index.lock. - git refusing an action (a numeric exit status) answers 409 EGIT with git's stderr instead of a 500 carrying a numeric code; one shared parseNumstat. Client: - Changes panel: no stale answer from a previous lane or an older quick-open query repaints the panel; discard disarms on lane switch; Enter on a file path opens it instead of folding; bounded tree expand-all. - Large diffs (> 500 rows) render only the visible rows, with stable width and in-sync split columns; mirrored split scroll is clamped. - Git bar: no "in sync" without counts, a clear no-remote message, branch list re-read after a switch, and no previous-lane facts after a lane switch. - Tooltips no longer flicker or close on unrelated scrolls.
This commit is contained in:
+55
-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, { timeout = 0 } = {}) {
|
||||
async function git(cwd, args, { timeout = 0, maxBuffer = 8 * 1024 * 1024, literal = false } = {}) {
|
||||
// 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 };
|
||||
@@ -52,12 +52,23 @@ async function git(cwd, args, { timeout = 0 } = {}) {
|
||||
}
|
||||
// Prevent credential prompts from hanging a background provisioning job
|
||||
env.GIT_TERMINAL_PROMPT = "0";
|
||||
// Pathspec behaviour is ours to choose, never the parent's. `literal` makes a
|
||||
// path argument name exactly one file: without it `new*.js` or `app/[id]/`
|
||||
// is a GLOB, and `git clean -- 'new*.js'` deletes every match.
|
||||
delete env.GIT_GLOB_PATHSPECS;
|
||||
delete env.GIT_NOGLOB_PATHSPECS;
|
||||
delete env.GIT_ICASE_PATHSPECS;
|
||||
if (literal) env.GIT_LITERAL_PATHSPECS = "1";
|
||||
else delete env.GIT_LITERAL_PATHSPECS;
|
||||
|
||||
try {
|
||||
const result = await execFileAsync("git", args, {
|
||||
cwd,
|
||||
env,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
// A read with its own size cap passes a bigger buffer and treats an
|
||||
// overflow (err.code ERR_CHILD_PROCESS_STDIO_MAXBUFFER, partial output in
|
||||
// err.git.stdout) as truncated rather than as a failure.
|
||||
maxBuffer,
|
||||
// 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.
|
||||
@@ -631,6 +642,46 @@ async function statusCounts(dir) {
|
||||
return { dirty, untracked, head, files };
|
||||
}
|
||||
|
||||
/**
|
||||
* The lane card's view of the working copy: only entries inside the lane's own
|
||||
* cwd, cwd-relative and read with `-z` — the same scoping the Changes panel
|
||||
* uses (`laneStatus`), so a subdirectory lane's card never counts or lists a
|
||||
* sibling's files, and a non-ASCII name is not shown C-quoted. `statusCounts`
|
||||
* stays whole-repo on purpose: `switch` and the preflight need to know about
|
||||
* ANY dirt a branch change would carry across.
|
||||
*/
|
||||
async function laneFiles(dir) {
|
||||
// Lazy: git-diff.js requires this module, so a top-level require would cycle.
|
||||
const { laneStatus, parseNumstat } = require("./git-diff");
|
||||
const entries = await laneStatus(dir);
|
||||
let stats = new Map();
|
||||
try {
|
||||
// -z --relative: cwd-scoped, cwd-relative, unquoted — keys the entries.
|
||||
stats = parseNumstat(
|
||||
(await git(dir, ["diff", "--numstat", "-z", "--relative", "HEAD"])).stdout
|
||||
);
|
||||
} catch {
|
||||
/* no HEAD to diff against -> no per-file deltas */
|
||||
}
|
||||
let dirty = 0;
|
||||
let untracked = 0;
|
||||
const files = [];
|
||||
for (const e of entries) {
|
||||
if (e.code === "??") untracked++;
|
||||
else dirty++;
|
||||
if (files.length < STATUS_FILE_CAP) {
|
||||
files.push({
|
||||
path: e.path,
|
||||
status: e.code,
|
||||
// Counts only: the card's file record has no `binary` field.
|
||||
added: stats.get(e.path)?.added ?? null,
|
||||
deleted: stats.get(e.path)?.deleted ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { dirty, untracked, files };
|
||||
}
|
||||
|
||||
/**
|
||||
* What a lane's working copy looks like right now: which branch it is on, the
|
||||
* short HEAD, that commit's subject, and how much is uncommitted.
|
||||
@@ -641,7 +692,8 @@ async function statusCounts(dir) {
|
||||
* caller shows what git says rather than inventing a nicer word for it.
|
||||
*/
|
||||
async function gitFacts(dir) {
|
||||
const { dirty, untracked, head, files } = await statusCounts(dir);
|
||||
const { head } = await statusCounts(dir);
|
||||
const { dirty, untracked, files } = await laneFiles(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, ...(await trackingFacts(dir)) };
|
||||
|
||||
Reference in New Issue
Block a user