fix(lanes): cloning into a deleted workspace's folder re-attaches it instead of failing UNIQUE
A workspace record outlives its folder (deleted from the File Explorer or a
shell). Cloning the same repo into that path again ran `git clone`, then
createLane hit idx_lanes_cwd_tool and answered 500 "UNIQUE constraint failed:
lanes.cwd, lanes.tool", leaving the new clone with no workspace on it.
POST /api/lanes/clone now looks for the caller's own workspace at exactly
that folder and tool and answers 200 { lane, created: false, path }, like
POST /ensure. Another user's workspace there is still refused before the
clone (folderTakenByOther), and a nested path still makes its own workspace.
This commit is contained in:
@@ -405,6 +405,7 @@ closing the dialog then only hides it behind a progress chip (click to reopen)
|
||||
```
|
||||
POST /api/lanes/clone Body: { url, parent, name?, title?, tool?, credentials?: { username, password, remember? }, partial?, progressToken? }
|
||||
→ 201 { lane, created: true, path }
|
||||
→ 200 { lane, created: false, path } (the user's own workspace already at that folder + tool, its folder deleted since)
|
||||
GET /api/lanes/clone/progress/:token
|
||||
→ 200 { state: queued|running|done|failed, position, running, waiting, limit,
|
||||
phase: remote|receiving|resolving|checkout|other|null, label, percent, detail }
|
||||
|
||||
@@ -143,6 +143,25 @@ describe("POST /api/lanes/clone", () => {
|
||||
assert.equal(again.body.error.code, "EEXISTS");
|
||||
});
|
||||
|
||||
it("re-attaches a workspace whose folder was deleted instead of failing UNIQUE", async () => {
|
||||
const first = await request("POST", "/api/lanes/clone", {
|
||||
url: "https://forge.test/o/r.git",
|
||||
parent: WORK,
|
||||
name: "gone",
|
||||
});
|
||||
assert.equal(first.status, 201, JSON.stringify(first.body));
|
||||
fs.rmSync(path.join(WORK, "gone"), { recursive: true, force: true });
|
||||
const again = await request("POST", "/api/lanes/clone", {
|
||||
url: "https://forge.test/o/r.git",
|
||||
parent: WORK,
|
||||
name: "gone",
|
||||
});
|
||||
assert.equal(again.status, 200, JSON.stringify(again.body));
|
||||
assert.equal(again.body.created, false);
|
||||
assert.equal(again.body.lane.id, first.body.lane.id);
|
||||
assert.ok(fs.existsSync(path.join(WORK, "gone", "README.md")));
|
||||
});
|
||||
|
||||
it("clones over ssh with the user's key, kept in the clone's config", async () => {
|
||||
const made = await request("POST", "/api/me/git/ssh-key", {});
|
||||
assert.equal(made.status, 201);
|
||||
|
||||
+11
-1
@@ -886,9 +886,19 @@ router.post("/clone", sameOriginGuard, async (req, res) => {
|
||||
});
|
||||
if (!done) return;
|
||||
const { target, name, ownerId } = done;
|
||||
const cwd = lanesLib.canonicalCwd(target);
|
||||
// A workspace can outlive its folder (deleted from the File Explorer or a
|
||||
// shell): cloning into that path again re-attaches the user's own record
|
||||
// rather than tripping idx_lanes_cwd_tool. Another user's was refused above.
|
||||
const strip = (p) => p.replace(/[\\/]+$/, "");
|
||||
const found = lanesLib.resolveLaneByCwd(cwd, body.tool || "claude");
|
||||
if (found && strip(found.cwd) === strip(cwd) && scope.canSeeLane(req, found.id)) {
|
||||
broadcastLane(found.id);
|
||||
return res.status(200).json({ lane: payload(found), created: false, path: target });
|
||||
}
|
||||
try {
|
||||
const lane = lanesLib.createLane({
|
||||
cwd: lanesLib.canonicalCwd(target),
|
||||
cwd,
|
||||
owner_id: ownerId,
|
||||
title: typeof body.title === "string" && body.title.trim() ? body.title.trim() : name,
|
||||
...(body.tool ? { tool: body.tool } : {}),
|
||||
|
||||
Reference in New Issue
Block a user