Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tools]
bun = "latest"
bun = "1.3.8"

[env]
_.path = ["bin"]
7 changes: 7 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"typescript": "^5"
},
"dependencies": {
"@clack/prompts": "^1.1.0",
"commander": "^14.0.3"
}
}
104 changes: 31 additions & 73 deletions src/commands/add.test.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,32 @@
import { afterEach, beforeEach, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { $, type ShellExpression } from "bun";

const binDir = resolve(import.meta.dir, "../../bin");

const testEnv = {
...Bun.env,
PATH: `${binDir}:${Bun.env.PATH}`,
GIT_AUTHOR_NAME: "test",
GIT_AUTHOR_EMAIL: "test@test.com",
GIT_COMMITTER_NAME: "test",
GIT_COMMITTER_EMAIL: "test@test.com",
};

let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "git-witty-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true });
});

function sh(strings: TemplateStringsArray, ...values: ShellExpression[]) {
return $(strings, ...values)
.cwd(tempDir)
.env(testEnv)
.quiet();
}

async function setupRepo(): Promise<{ repo: string; primaryBranch: string }> {
const origin = "origin";
const repo = "my-repo";

await sh`git init ${origin}`;
await sh`git -C ${origin} commit --allow-empty -m "init"`;
await sh`git witty clone ${origin} ${repo}`;

const primaryBranch = (
await sh`git -C ${repo}/.bare symbolic-ref --short HEAD`.text()
).trim();

return { repo, primaryBranch };
}

test("add creates a worktree for an existing branch", async () => {
const { repo, primaryBranch } = await setupRepo();

await sh`git -C ${join(repo, primaryBranch)} branch feature-a`;
await sh`cd ${join(repo, primaryBranch)} && git witty add feature-a`;

const branch = (
await sh`git -C ${join(repo, "feature-a")} branch --show-current`.text()
).trim();
expect(branch).toBe("feature-a");
});

test("add creates a worktree that is listed", async () => {
const { repo, primaryBranch } = await setupRepo();

await sh`git -C ${join(repo, primaryBranch)} branch feature-b`;
await sh`cd ${join(repo, primaryBranch)} && git witty add feature-b`;

const worktreeLines = (await sh`git -C ${repo} worktree list`.text())
.trim()
.split("\n");

expect(worktreeLines).toHaveLength(3);
expect(worktreeLines.some((l) => l.includes("[feature-b]"))).toBe(true);
import { describe, expect, test } from "bun:test";
import { join } from "node:path";
import { createRepo, useTestDir } from "../test-helpers";

describe("add", () => {
const ctx = useTestDir();

test("creates worktree as sibling from nested directory", async () => {
const { name: origin, branch } = await createRepo(ctx.sh, {
branch: "main",
});
const target = "my-repo";
await ctx.sh`git witty clone ${origin} ${target}`;

// Run add from a nested subdirectory inside an existing worktree
const nested = join(ctx.dir, target, branch, "some", "dir");
await ctx.sh`mkdir -p ${nested}`;
const sh = ctx.at(nested);

// Create the branch in the origin so git worktree add can find it
await ctx.sh`git -C ${origin} branch feature`;

await sh`git witty add feature`;

// Worktree should be at my-repo/feature, not nested under cwd
const worktreeDir = join(ctx.dir, target, "feature");
const currentBranch = (
await ctx.sh`git -C ${worktreeDir} branch --show-current`.text()
).trim();
expect(currentBranch).toBe("feature");
});
});
26 changes: 13 additions & 13 deletions src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { join } from "node:path";
import { $ } from "bun";
import { getRepoName, getRepoRoot } from "../repo";
import { addFolder } from "../workspace";
import { Git } from "../git";
import { syncWorkspace } from "../workspace";

export async function add({ branch }: { branch: string }) {
const root = await getRepoRoot();
const repoName = await getRepoName();
const worktreePath = join(root, branch);

await $`git worktree add ${worktreePath}`;
await $`git -C ${worktreePath} branch --set-upstream-to=origin/${branch}`
.quiet()
.nothrow();
await addFolder(root, repoName, worktreePath);
export async function add(branch: string) {
const git = await new Git().root();
const root = await git.rootDir();
const repoName = await git.repoName();
await git.worktree("add", join(root, branch));
await git
.C(join(root, branch))
.exec("branch", `--set-upstream-to=origin/${branch}`)
.nothrow()
.quiet();
await syncWorkspace(root, repoName);
}
193 changes: 77 additions & 116 deletions src/commands/clone.test.ts
Original file line number Diff line number Diff line change
@@ -1,117 +1,78 @@
import { afterEach, beforeEach, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { $, type ShellExpression } from "bun";

const binDir = resolve(import.meta.dir, "../../bin");

const testEnv = {
...Bun.env,
PATH: `${binDir}:${Bun.env.PATH}`,
GIT_AUTHOR_NAME: "test",
GIT_AUTHOR_EMAIL: "test@test.com",
GIT_COMMITTER_NAME: "test",
GIT_COMMITTER_EMAIL: "test@test.com",
};

let tempDir: string;

beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "git-witty-"));
});

afterEach(async () => {
await rm(tempDir, { recursive: true });
});

function sh(strings: TemplateStringsArray, ...values: ShellExpression[]) {
return $(strings, ...values)
.cwd(tempDir)
.env(testEnv)
.quiet();
}

test("clone creates worktree layout from local repo", async () => {
const origin = "origin";
const target = "my-repo";

await sh`git init ${origin}`;
await sh`git -C ${origin} commit --allow-empty -m "init"`;

await sh`git witty clone ${origin} ${target}`;

// Verify .git pointer file we create
expect(await Bun.file(join(tempDir, target, ".git")).text()).toBe(
"gitdir: ./.bare\n",
);

// Verify worktree is a functional git checkout on the primary branch
const branch = (
await sh`git -C ${target}/.bare symbolic-ref --short HEAD`.text()
).trim();
const worktreeBranch = (
await sh`git -C ${join(target, branch)} branch --show-current`.text()
).trim();
expect(worktreeBranch).toBe(branch);

// Verify both bare repo and worktree are registered
const worktreeLines = (await sh`git -C ${target} worktree list`.text())
.trim()
.split("\n");
expect(worktreeLines).toHaveLength(2);
expect(worktreeLines[0]).toContain(".bare");
expect(worktreeLines[1]).toContain(`[${branch}]`);
});

test("clone infers repo name from url", async () => {
const origin = "origin/my-project";
const targetDir = "target";

await sh`git init ${origin}`;
await sh`git -C ${origin} commit --allow-empty -m "init"`;

await sh`mkdir -p ${targetDir}`;
await sh`cd ${targetDir} && git witty clone ${join(tempDir, origin)}`;

expect(
await Bun.file(join(tempDir, targetDir, "my-project", ".git")).text(),
).toBe("gitdir: ./.bare\n");
});

test("clone creates a workspace file", async () => {
const origin = "origin";
const target = "my-repo";

await sh`git init ${origin}`;
await sh`git -C ${origin} commit --allow-empty -m "init"`;

await sh`git witty clone ${origin} ${target}`;

const branch = (
await sh`git -C ${target}/.bare symbolic-ref --short HEAD`.text()
).trim();

const wsFile = Bun.file(join(tempDir, target, `${origin}.code-workspace`));
expect(await wsFile.exists()).toBe(true);

const workspace = await wsFile.json();
expect(workspace.folders).toEqual([{ path: branch }]);
});

test("clone creates worktree for non-main primary branch", async () => {
const origin = "origin";
const target = "my-repo";
const branch = "develop";

await sh`git init -b ${branch} ${origin}`;
await sh`git -C ${origin} commit --allow-empty -m "init"`;

await sh`git witty clone ${origin} ${target}`;

const worktreeBranch = (
await sh`git -C ${join(target, branch)} branch --show-current`.text()
).trim();
expect(worktreeBranch).toBe(branch);
expect(await Bun.file(join(tempDir, target, "main")).exists()).toBe(false);
import { describe, expect, test } from "bun:test";
import { join } from "node:path";
import { BARE_DIR, GITDIR_POINTER } from "../git";
import { createRepo, useTestDir } from "../test-helpers";

describe("clone", () => {
const ctx = useTestDir();

test("creates worktree layout from local repo", async () => {
const { name: origin, branch } = await createRepo(ctx.sh, {
branch: "main",
});
const target = "my-repo";

await ctx.sh`git witty clone ${origin} ${target}`;

// Verify .git pointer file we create
expect(await Bun.file(join(ctx.dir, target, ".git")).text()).toBe(
GITDIR_POINTER,
);

// Verify worktree is a functional git checkout on the primary branch
const worktreeBranch = (
await ctx.sh`git -C ${join(target, branch)} branch --show-current`.text()
).trim();
expect(worktreeBranch).toBe(branch);

// Verify both bare repo and worktree are registered
const worktreeLines = (await ctx.sh`git -C ${target} worktree list`.text())
.trim()
.split("\n");
expect(worktreeLines).toHaveLength(2);
expect(worktreeLines[0]).toContain(BARE_DIR);
expect(worktreeLines[1]).toContain(`[${branch}]`);
});

test("infers repo name from url", async () => {
await createRepo(ctx.sh, { name: "origin/my-project", branch: "main" });
const targetDir = "target";

await ctx.sh`mkdir -p ${targetDir}`;
await ctx.at(
join(ctx.dir, targetDir),
)`git witty clone ${join(ctx.dir, "origin/my-project")}`;

expect(
await Bun.file(join(ctx.dir, targetDir, "my-project", ".git")).text(),
).toBe(GITDIR_POINTER);
});

test("creates a workspace file", async () => {
const { name: origin, branch } = await createRepo(ctx.sh, {
branch: "main",
});
const target = "my-repo";

await ctx.sh`git witty clone ${origin} ${target}`;

const wsPath = join(ctx.dir, target, `${target}.code-workspace`);
const workspace = await Bun.file(wsPath).json();
expect(workspace.folders).toEqual([{ path: branch }]);
});

test("creates worktree for non-main primary branch", async () => {
const { name: origin, branch } = await createRepo(ctx.sh, {
branch: "develop",
});
const target = "my-repo";

await ctx.sh`git witty clone ${origin} ${target}`;

const worktreeBranch = (
await ctx.sh`git -C ${join(target, branch)} branch --show-current`.text()
).trim();
expect(worktreeBranch).toBe(branch);
expect(await Bun.file(join(ctx.dir, target, "main")).exists()).toBe(false);
});
});
Loading
Loading