mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-29 04:32:57 +00:00
feat(worktrees): copy included local files before setup and agents (#8100)
* feat(worktrees): copy included local files before setup and agents Keep local-file materialization in one built-in lifecycle hook so workspace setup and the first agent see the completed copy. Reuse the community plugin algorithm with the source checkout supplied by the worktree hook. Co-authored-by: Carlos Precioso <github@precioso.design> * test(plugins): make worktree lifecycle checks portable on Windows Compare source checkout filesystem identity across Git and Windows path spellings. Exercise copy rollback with a destination file blocking a directory, since Git for Windows accepts directories as exclude files. --------- Co-authored-by: Carlos Precioso <github@precioso.design>
This commit is contained in:
co-authored by
Carlos Precioso
parent
986687b8fd
commit
dee0d138e9
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Carlos Precioso
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
||||
import { copyWorktreeIncludes } from "./server/copy.js";
|
||||
|
||||
export default function contribute(server: PluginServerContext) {
|
||||
return server.addHook("worktree.create.after", async ({ worktree }) => {
|
||||
await copyWorktreeIncludes(worktree.path, worktree.sourceCheckout);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "id": "worktree-include", "requirements": { "paseo": ">=0.14.0-beta.1" } }
|
||||
@@ -0,0 +1,71 @@
|
||||
// Derived from cprecioso/paseo-worktreeinclude (MIT); see ./LICENSE in the plugin root.
|
||||
// Copyright (c) 2026 Carlos Precioso.
|
||||
import { execFile } from "node:child_process";
|
||||
import { constants, existsSync } from "node:fs";
|
||||
import { copyFile, mkdir } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const INCLUDE_FILE = ".worktreeinclude";
|
||||
|
||||
export type CopyResult =
|
||||
| { kind: "no-include-file" }
|
||||
| { kind: "copied"; copied: string[]; skipped: string[] };
|
||||
|
||||
async function git(cwd: string, args: string[], input?: string): Promise<string> {
|
||||
const child = exec("git", args, { cwd, maxBuffer: 64 * 1024 * 1024 });
|
||||
if (input !== undefined) {
|
||||
child.child.stdin?.end(input);
|
||||
}
|
||||
const { stdout } = await child;
|
||||
return stdout;
|
||||
}
|
||||
|
||||
async function gitAllowExit1(cwd: string, args: string[], input: string): Promise<string> {
|
||||
try {
|
||||
return await git(cwd, args, input);
|
||||
} catch (error) {
|
||||
// `git check-ignore` exits 1 when nothing matched.
|
||||
if ((error as { code?: number }).code === 1) return "";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const splitNul = (s: string) => s.split("\0").filter(Boolean);
|
||||
|
||||
// Copy files that match .worktreeinclude (gitignore syntax)
|
||||
// AND are gitignored, so tracked files are never touched.
|
||||
export async function copyWorktreeIncludes(worktree: string, source: string): Promise<CopyResult> {
|
||||
if (!existsSync(join(source, INCLUDE_FILE))) return { kind: "no-include-file" };
|
||||
|
||||
const matches = splitNul(
|
||||
await git(source, [
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--ignored",
|
||||
`--exclude-from=${INCLUDE_FILE}`,
|
||||
"-z",
|
||||
]),
|
||||
);
|
||||
if (matches.length === 0) return { kind: "copied", copied: [], skipped: [] };
|
||||
|
||||
const ignored = splitNul(
|
||||
await gitAllowExit1(source, ["check-ignore", "--stdin", "-z"], matches.join("\0") + "\0"),
|
||||
);
|
||||
|
||||
const copied: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
for (const file of ignored) {
|
||||
const dest = join(worktree, file);
|
||||
if (existsSync(dest)) {
|
||||
skipped.push(file);
|
||||
continue;
|
||||
}
|
||||
await mkdir(dirname(dest), { recursive: true });
|
||||
// Clone (APFS/btrfs copy-on-write) when supported, else fall back to a regular copy.
|
||||
await copyFile(join(source, file), dest, constants.COPYFILE_FICLONE);
|
||||
copied.push(file);
|
||||
}
|
||||
return { kind: "copied", copied, skipped };
|
||||
}
|
||||
Reference in New Issue
Block a user