feat(lanes): Add Lane repo/worktree mode toggle, manual branch, folder browse
- Repo mode adopts a directory as-is via /lanes/ensure (no worktree, no branch fields) - the right choice for a main repo you want stage detection on. Worktree mode (default) keeps the existing provisioning flow but now requires a manually-typed branch name instead of deriving one from the title. - POST /lanes/worktree accepts an optional `branch`, validated via `git check-ref-format --branch`; omitting it preserves the CLI's existing auto-derived-branch behavior. - New GET /lanes/browse lists a directory's immediate subdirectories, backing a small folder-browse modal on both path fields - browsers cannot expose an absolute path from a native picker, so this is server-backed instead, consistent with the tool's local-first model.
This commit is contained in:
@@ -488,6 +488,68 @@ describe("managed worktree provisioning", () => {
|
||||
const missing = await request("GET", `/api/lanes/${lane.id}`);
|
||||
assert.equal(missing.status, 404);
|
||||
});
|
||||
|
||||
it("uses a caller-supplied branch name instead of deriving one from the slug", async () => {
|
||||
const created = await request("POST", "/api/lanes/worktree", {
|
||||
sourceRepo: SRC,
|
||||
title: "Custom Branch",
|
||||
base: "main",
|
||||
branch: "custom/my-branch",
|
||||
});
|
||||
|
||||
assert.equal(created.status, 202);
|
||||
assert.equal(created.body.lane.branch, "custom/my-branch");
|
||||
const lane = await waitForProvisioning(created.body.lane.id);
|
||||
assert.equal(lane.status, "idle");
|
||||
assert.equal(g(lane.cwd, "branch", "--show-current").trim(), "custom/my-branch");
|
||||
});
|
||||
|
||||
it("rejects an invalid caller-supplied branch name before creating anything", async () => {
|
||||
const response = await request("POST", "/api/lanes/worktree", {
|
||||
sourceRepo: SRC,
|
||||
title: "Bad Branch",
|
||||
base: "main",
|
||||
branch: "not a valid branch..name",
|
||||
});
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(response.body.error.code, "EBADBRANCH");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/lanes/browse", () => {
|
||||
it("lists a directory's immediate subdirectories, marking git repos", async () => {
|
||||
const response = await request("GET", `/api/lanes/browse?path=${encodeURIComponent(ROOT)}`);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.body.path, ROOT);
|
||||
const names = response.body.entries.map((e) => e.name);
|
||||
assert.ok(names.includes("src-repo"));
|
||||
const srcRepoEntry = response.body.entries.find((e) => e.name === "src-repo");
|
||||
assert.equal(srcRepoEntry.isGitRepo, true);
|
||||
});
|
||||
|
||||
it("reports the parent directory, or null at the filesystem root", async () => {
|
||||
const response = await request("GET", `/api/lanes/browse?path=${encodeURIComponent(ROOT)}`);
|
||||
assert.equal(response.body.parent, path.dirname(ROOT));
|
||||
|
||||
const rootResponse = await request("GET", "/api/lanes/browse?path=/");
|
||||
assert.equal(rootResponse.body.parent, null);
|
||||
});
|
||||
|
||||
it("rejects a path that does not exist or is not a directory", async () => {
|
||||
const missing = await request(
|
||||
"GET",
|
||||
`/api/lanes/browse?path=${encodeURIComponent(path.join(ROOT, "does-not-exist"))}`
|
||||
);
|
||||
assert.equal(missing.status, 400);
|
||||
assert.equal(missing.body.error.code, "ENOTFOUND");
|
||||
|
||||
const filePath = path.join(ROOT, "a-file.txt");
|
||||
fs.writeFileSync(filePath, "hi\n");
|
||||
const notADir = await request("GET", `/api/lanes/browse?path=${encodeURIComponent(filePath)}`);
|
||||
assert.equal(notADir.status, 400);
|
||||
assert.equal(notADir.body.error.code, "ENOTADIR");
|
||||
});
|
||||
});
|
||||
|
||||
describe("destructive lane lifecycle actions", () => {
|
||||
|
||||
Reference in New Issue
Block a user