Public: anyone can read this; the crew can change it.

A moved repository shows one line in its readers' feed: "moved from alice/demo to acme/demo" #10113

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/10100-a-moved-repository-shows-one-line-in-its → main 2 commits fork 1 thread · 1 unresolved

  • Merged. mason/10100-a-moved-repository-shows-one-line-in-its was merged into main as fe56269.

Review

Sign in to review this change.
3 files changed +69 −0
docs/features/repo-transfer.md +4
@@ -21,6 +21,10 @@ Who moved what from where to where is kept twice: an audit row in
2121 `repo_transfer`, written in the move's own transaction and never rewritten,
2222 and a `repo.transferred` event for the repository's readers.
2323
24 The event appears in their feed (and the dashboard, which shares it) as one
25 line, "moved from alice/demo to acme/demo", linking to the new path. Someone
26 who cannot read the repository never sees it.
27
2428 Old paths keep working: a web or API request under the old `owner/name`
2529 answers `308` to the same path under the new owner, for reads and writes
2630 alike (a write is never applied silently; the client re-sends it). Someone
src/feed/items.ts +8
@@ -725,6 +725,14 @@ function fromEvent(
725725 link: `${base(repo)}/map`,
726726 what: clip(typeof p["why"] === "string" ? p["why"] : "the crew's slots changed"),
727727 };
728 // A move is news to the repository's readers; the link is the new path.
729 case "repo.transferred": {
730 const from = typeof p["from"] === "string" ? p["from"] : "";
731 const to = typeof p["to"] === "string" ? p["to"] : "";
732 if (from === "" || to === "") return undefined;
733 const what = `moved from ${clip(from, 60)} to ${clip(to, 60)}`;
734 return { ...common, kind: "map", link: base(repo), what, said: what };
735 }

Fine as is. The feed and dashboard share this item, so both doors show it; the how-to in docs/features/repo-transfer.md needs a line saying so.

728736 default:
729737 return undefined;
730738 }
test/feed-transferred.test.ts added +57
@@ -0,0 +1,57 @@
1 /** A moved repository is one line in its readers' feed, and not in a stranger's (#10100). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtempSync, rmSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { transferRepository } from "../src/actions/transfer.ts";
8 import { createApp, type App } from "../src/app.ts";
9 import { createUser, type User } from "../src/core/users.ts";
10 import { readFeed } from "../src/feed/feed.ts";
11 import { quietLogs } from "./helpers/globals.ts";
12
13 quietLogs();
14
15 let root = "";
16 let app: App;
17 let alice: User;
18 let bob: User;
19 let carol: User;
20
21 beforeAll(async () => {
22 root = mkdtempSync(join(tmpdir(), "forge-feed-moved-"));
23 app = createApp({ dataDir: root, port: 0 });
24 const make = async (username: string, admin: boolean): Promise<User> => {
25 const u = await createUser(app.db, {
26 origin: "seed",
27 trusted: true,
28 username,
29 email: `${username}@forge.local`,
30 password: "a-good-passphrase",
31 adminIfFirst: admin,
32 });
33 if (!u.ok) throw new Error(u.error);
34 return u.value;
35 };
36 alice = await make("alice", true);
37 bob = await make("bob", false);
38 carol = await make("carol", false);
39 });
40 afterAll(() => {
41 app.close();
42 rmSync(root, { recursive: true, force: true });
43 });
44
45 test("the move reads as one line for the new owner, and a stranger never sees it", async () => {
46 const made = await createRepo(app, alice, { name: "demo", visibility: "private" });
47 if (!made.ok) throw new Error(String(made.error));
48 const moved = await transferRepository(app, alice, made.value, "bob");
49 if (!moved.ok) throw new Error(String(moved.error));
50 const lines = (u: User) =>
51 readFeed(app.db, u, { limit: 50 }).items.filter((i) => i.what.includes("moved from"));
52 const mine = lines(bob);
53 expect(mine).toHaveLength(1);
54 expect(mine[0]!.what).toBe("moved from alice/demo to bob/demo");
55 expect(mine[0]!.link).toBe("/bob/demo");
56 expect(lines(carol)).toHaveLength(0);
57 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs