-
Notifications
You must be signed in to change notification settings - Fork 0
feat(setup): configure a repo with worktree setup instead of hand-writing .worktreerc #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "worktree-cli": minor | ||
| --- | ||
|
|
||
| Add `worktree setup` to configure `.worktreerc` interactively (base branch, worktree dir, gitignore wiring) instead of writing the file by hand |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| import { afterEach, describe, expect, it, setDefaultTimeout } from "bun:test"; | ||
| import fs from "node:fs/promises"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { run } from "../lib/shell"; | ||
|
|
||
| type TestRepository = { | ||
| directory: string; | ||
| root: string; | ||
| }; | ||
|
|
||
| const cleanupDirectories: string[] = []; | ||
| const cliPath = path.resolve(import.meta.dir, "../index.ts"); | ||
| const INTEGRATION_TEST_TIMEOUT_MS = 20_000; | ||
|
|
||
| setDefaultTimeout(INTEGRATION_TEST_TIMEOUT_MS); | ||
|
|
||
| async function createTestRepository(): Promise<TestRepository> { | ||
| const directory = await fs.mkdtemp( | ||
| path.join(os.tmpdir(), "worktree-setup-test-") | ||
| ); | ||
| cleanupDirectories.push(directory); | ||
|
|
||
| const remote = path.join(directory, "remote.git"); | ||
| const root = path.join(directory, "repository"); | ||
| await fs.mkdir(root); | ||
|
|
||
| await run("git", ["init", "--bare", "-b", "main", remote]); | ||
| await run("git", ["init", "-b", "main"], { cwd: root }); | ||
| await run("git", ["config", "user.name", "Test User"], { cwd: root }); | ||
| await run("git", ["config", "user.email", "test@example.com"], { | ||
| cwd: root, | ||
| }); | ||
|
|
||
| await fs.writeFile(path.join(root, "app.ts"), "export const value = 1;\n"); | ||
| await run("git", ["add", "."], { cwd: root }); | ||
| await run("git", ["commit", "-m", "initial"], { cwd: root }); | ||
| await run("git", ["remote", "add", "origin", remote], { cwd: root }); | ||
| await run("git", ["push", "-u", "origin", "main"], { cwd: root }); | ||
| await run("git", ["remote", "set-head", "origin", "-a"], { cwd: root }); | ||
|
|
||
| return { directory, root }; | ||
| } | ||
|
|
||
| async function runSetup( | ||
| root: string, | ||
| args: string[] | ||
| ): Promise<{ stderr: string; exitCode: number }> { | ||
| const cliProcess = Bun.spawn( | ||
| [process.execPath, "run", cliPath, "setup", ...args], | ||
| { | ||
| cwd: root, | ||
| env: { ...Bun.env, WORKTREE_NO_UPDATE: "1", NO_COLOR: "1" }, | ||
| stdin: "ignore", | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| } | ||
| ); | ||
| const [stderr, exitCode] = await Promise.all([ | ||
| new Response(cliProcess.stderr).text(), | ||
| cliProcess.exited, | ||
| ]); | ||
| return { stderr, exitCode }; | ||
| } | ||
|
|
||
| async function readFile(root: string, name: string): Promise<string | null> { | ||
| return fs.readFile(path.join(root, name), "utf8").catch(() => null); | ||
| } | ||
|
|
||
| afterEach(async () => { | ||
| await Promise.all( | ||
| cleanupDirectories.splice(0).map(function (directory) { | ||
| return fs.rm(directory, { recursive: true, force: true }); | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| describe("setup command", () => { | ||
| it("writes .worktreerc and wires up gitignores on first run", async () => { | ||
| const { root } = await createTestRepository(); | ||
|
|
||
| const result = await runSetup(root, ["--base", "main", "--yes"]); | ||
|
|
||
| expect(result.exitCode).toBe(0); | ||
| expect(result.stderr).toContain("Setup complete."); | ||
| const rc = await readFile(root, ".worktreerc"); | ||
| expect(rc).toContain("DEFAULT_BASE=origin/main"); | ||
| expect(rc).toContain("WORKTREE_DIR=.worktrees"); | ||
| expect(await readFile(root, ".worktrees/.gitignore")).toBe("*\n"); | ||
| expect(await readFile(root, ".gitignore")).toContain(".worktrees/\n"); | ||
| }); | ||
|
|
||
| it("resolves origin/HEAD when --yes has no configured base", async () => { | ||
| const { root } = await createTestRepository(); | ||
|
|
||
| const result = await runSetup(root, ["--yes"]); | ||
|
|
||
| expect(result.exitCode).toBe(0); | ||
| expect(await readFile(root, ".worktreerc")).toContain( | ||
| "DEFAULT_BASE=origin/main" | ||
| ); | ||
| }); | ||
|
|
||
| it("rejects an unknown base without writing config", async () => { | ||
| const { root } = await createTestRepository(); | ||
|
|
||
| const result = await runSetup(root, ["--base", "nope", "--yes"]); | ||
|
|
||
| expect(result.exitCode).toBe(1); | ||
| expect(await readFile(root, ".worktreerc")).toBeNull(); | ||
| expect( | ||
| await fs.stat(path.join(root, ".worktrees")).catch(() => null) | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it("rejects a stale configured base in --yes mode", async () => { | ||
| const { root } = await createTestRepository(); | ||
| await fs.writeFile( | ||
| path.join(root, ".worktreerc"), | ||
| "DEFAULT_BASE=origin/stale\n" | ||
| ); | ||
|
|
||
| const result = await runSetup(root, ["--yes"]); | ||
|
|
||
| expect(result.exitCode).toBe(1); | ||
| expect(await readFile(root, ".worktreerc")).toBe( | ||
| "DEFAULT_BASE=origin/stale\n" | ||
| ); | ||
| }); | ||
|
|
||
| it("preserves unknown keys and comments when rewriting", async () => { | ||
| const { root } = await createTestRepository(); | ||
| await fs.writeFile( | ||
| path.join(root, ".worktreerc"), | ||
| "# team defaults\nCUSTOM=keepme\nDEFAULT_BASE=origin/main\n" | ||
| ); | ||
|
|
||
| const result = await runSetup(root, ["--yes"]); | ||
|
|
||
| expect(result.exitCode).toBe(0); | ||
| const rc = await readFile(root, ".worktreerc"); | ||
| expect(rc).toContain("# team defaults"); | ||
| expect(rc).toContain("CUSTOM=keepme"); | ||
| expect(rc).toContain("DEFAULT_BASE=origin/main"); | ||
| }); | ||
|
|
||
| it("rejects a directory that escapes the repository", async () => { | ||
| const { directory, root } = await createTestRepository(); | ||
|
|
||
| const result = await runSetup(root, [ | ||
| "--base", | ||
| "main", | ||
| "--worktree-dir", | ||
| "../evil", | ||
| "--yes", | ||
| ]); | ||
|
|
||
| expect(result.exitCode).toBe(1); | ||
| expect(await readFile(root, ".worktreerc")).toBeNull(); | ||
| expect( | ||
| await fs.stat(path.join(directory, "evil")).catch(() => null) | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it("rejects the repository root as the worktree directory", async () => { | ||
| const { root } = await createTestRepository(); | ||
|
|
||
| const result = await runSetup(root, [ | ||
| "--base", | ||
| "main", | ||
| "--worktree-dir", | ||
| ".", | ||
| "--yes", | ||
| ]); | ||
|
|
||
| expect(result.exitCode).toBe(1); | ||
| expect(await readFile(root, ".worktreerc")).toBeNull(); | ||
| }); | ||
|
|
||
| it("fails before writing config when the directory cannot be created", async () => { | ||
| const { root } = await createTestRepository(); | ||
| await fs.writeFile(path.join(root, ".worktrees"), "in the way\n"); | ||
|
|
||
| const result = await runSetup(root, ["--base", "main", "--yes"]); | ||
|
|
||
| expect(result.exitCode).toBe(1); | ||
| expect(await readFile(root, ".worktreerc")).toBeNull(); | ||
| }); | ||
|
|
||
| it("rejects a worktree directory symlinked outside the repository", async () => { | ||
| const { directory, root } = await createTestRepository(); | ||
| const external = path.join(directory, "external"); | ||
| await fs.mkdir(external); | ||
| await fs.symlink(external, path.join(root, ".worktrees")); | ||
|
|
||
| const result = await runSetup(root, ["--base", "main", "--yes"]); | ||
|
|
||
| expect(result.exitCode).toBe(1); | ||
| expect(await readFile(root, ".worktreerc")).toBeNull(); | ||
| expect(await readFile(external, ".gitignore")).toBeNull(); | ||
| }); | ||
|
|
||
| it("honors a custom directory and stays idempotent on rerun", async () => { | ||
| const { root } = await createTestRepository(); | ||
|
|
||
| const first = await runSetup(root, [ | ||
| "--base", | ||
| "main", | ||
| "--worktree-dir", | ||
| ".wt", | ||
| "--yes", | ||
| ]); | ||
| expect(first.exitCode).toBe(0); | ||
| expect(await readFile(root, ".wt/.gitignore")).toBe("*\n"); | ||
|
|
||
| const second = await runSetup(root, ["--yes"]); | ||
| expect(second.exitCode).toBe(0); | ||
|
|
||
| const gitignore = await readFile(root, ".gitignore"); | ||
| expect( | ||
| gitignore?.split("\n").filter((line) => line === ".wt/") | ||
| ).toHaveLength(1); | ||
| expect(await readFile(root, ".worktreerc")).toContain( | ||
| "WORKTREE_DIR=.wt" | ||
| ); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.