5084334d81
Desktop-parity phase 6, following Claude Desktop: - the workspace home's Worktree checkbox starts the pane with `claude --worktree=<name>`, so Claude Code makes, populates (.worktreeinclude, WorktreeCreate hook) and cleans up <repo>/.claude/worktrees/<name>; AgentHub only excludes /.claude/worktrees/ at the repo root (Claude does not) - server/lib/session-worktree.js resolves a session's worktree from the session row only (owned by the workspace, registered by git worktree list); every lane git route takes ?session= and answers ENOWORKTREE instead of falling back to the main folder - the session view's Changes panel works in the opened run's worktree (dashboard_runs.worktree, GitTarget in the client api) - Resume/Fork start in the session's worktree; a gone one is ENOWORKTREE - Archive of a worktree session removes the worktree after asking: git worktree remove without --force, git branch -d (unmerged work keeps its branch), a lock held by a live pid refused, a stale one cleared - listWorktrees reads `locked <reason>` locks - tests scrub GIT_* env so the pre-commit suite cannot act on this repo
83 lines
3.1 KiB
JavaScript
83 lines
3.1 KiB
JavaScript
/**
|
|
* @file Keeps files AgentHub writes into a lane's directory out of git, through
|
|
* the one `info/exclude` git actually reads for that lane. That is the COMMON
|
|
* git dir's, not the lane's own: a linked worktree — every managed lane — has a
|
|
* private `.git/worktrees/<name>/`, and git never reads an exclude file there.
|
|
* A lane that is a subdirectory of its repo has no `.git` of its own at all.
|
|
*
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { execFileSync } = require("node:child_process");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
// Hook variables that would point git at ANOTHER repo (the one whose hook, or
|
|
// shell, started us) — the same set `worktree.js`'s git() scrubs. With
|
|
// GIT_DIR set, `rev-parse` would name that repo and the entry land there.
|
|
const REPO_VARS = [
|
|
"GIT_DIR",
|
|
"GIT_WORK_TREE",
|
|
"GIT_INDEX_FILE",
|
|
"GIT_COMMON_DIR",
|
|
"GIT_OBJECT_DIRECTORY",
|
|
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
"GIT_PREFIX",
|
|
"GIT_NAMESPACE",
|
|
];
|
|
|
|
/**
|
|
* Idempotently exclude `relPaths` (relative to `laneDir`), each anchored to the
|
|
* lane's own path so the entry hides nothing outside it. Best effort: a lane
|
|
* that is not a repo, or a git dir that cannot be written, is left alone —
|
|
* never a reason to fail the write that needed the exclude.
|
|
*
|
|
* @param {string} laneDir
|
|
* @param {string[]} relPaths e.g. [".mcp.json", ".claude/.cc-config-backups/"]
|
|
*/
|
|
function excludeFromGit(laneDir, relPaths) {
|
|
try {
|
|
const env = { ...process.env };
|
|
for (const name of REPO_VARS) delete env[name];
|
|
const [commonDir, prefix = ""] = execFileSync(
|
|
"git",
|
|
["rev-parse", "--git-common-dir", "--show-prefix"],
|
|
{ cwd: laneDir, env, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
).split("\n");
|
|
const lines = relPaths.map((rel) => `/${prefix}${rel}`);
|
|
const excludePath = path.join(path.resolve(laneDir, commonDir), "info", "exclude");
|
|
fs.mkdirSync(path.dirname(excludePath), { recursive: true });
|
|
let existing = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, "utf8") : "";
|
|
const present = new Set(existing.split("\n"));
|
|
const missing = lines.filter((line) => !present.has(line));
|
|
if (missing.length === 0) return;
|
|
if (existing && !existing.endsWith("\n")) existing += "\n";
|
|
fs.writeFileSync(excludePath, `${existing}${missing.join("\n")}\n`);
|
|
} catch {
|
|
/* not a repo, or unwritable — see above */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The same, anchored at the REPOSITORY root rather than `dir` — for paths a
|
|
* tool creates at the root whatever subdirectory it was started in (Claude
|
|
* Code's `.claude/worktrees/`). Best effort, like `excludeFromGit`.
|
|
*/
|
|
function excludeFromRepoRoot(dir, relPaths) {
|
|
try {
|
|
const env = { ...process.env };
|
|
for (const name of REPO_VARS) delete env[name];
|
|
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
cwd: dir,
|
|
env,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
}).trim();
|
|
if (root) excludeFromGit(root, relPaths);
|
|
} catch {
|
|
/* not a repo */
|
|
}
|
|
}
|
|
|
|
module.exports = { excludeFromGit, excludeFromRepoRoot };
|