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
15 changes: 9 additions & 6 deletions src/commands/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,35 @@ export function registerCloneCommand(program: Command, getCtx: () => ArbContext)
process.exit(1);
}

const result = await Bun.$`git clone ${url} ${target}`.quiet().nothrow();
const result = await Bun.$`git clone ${url} ${target}`.cwd(ctx.reposDir).quiet().nothrow();
if (result.exitCode !== 0) {
error(`Clone failed: ${result.stderr.toString().trim()}`);
process.exit(1);
}

await Bun.$`git -C ${target} checkout --detach`.quiet().nothrow();
await Bun.$`git -C ${target} checkout --detach`.cwd(target).quiet().nothrow();

if (options.upstream) {
// Add upstream remote
const addResult = await Bun.$`git -C ${target} remote add upstream ${options.upstream}`.quiet().nothrow();
const addResult = await Bun.$`git -C ${target} remote add upstream ${options.upstream}`
.cwd(target)
.quiet()
.nothrow();
if (addResult.exitCode !== 0) {
error(`Failed to add upstream remote: ${addResult.stderr.toString().trim()}`);
process.exit(1);
}

// Set remote.pushDefault so resolveRemotes() detects the fork layout
await Bun.$`git -C ${target} config remote.pushDefault origin`.quiet().nothrow();
await Bun.$`git -C ${target} config remote.pushDefault origin`.cwd(target).quiet().nothrow();

// Fetch upstream and auto-detect HEAD
const fetchResult = await Bun.$`git -C ${target} fetch upstream`.quiet().nothrow();
const fetchResult = await Bun.$`git -C ${target} fetch upstream`.cwd(target).quiet().nothrow();
if (fetchResult.exitCode !== 0) {
error(`Failed to fetch upstream: ${fetchResult.stderr.toString().trim()}`);
process.exit(1);
}
await Bun.$`git -C ${target} remote set-head upstream --auto`.quiet().nothrow();
await Bun.$`git -C ${target} remote set-head upstream --auto`.cwd(target).quiet().nothrow();

info(` publish: origin (${url})`);
info(` upstream: upstream (${options.upstream})`);
Expand Down
3 changes: 2 additions & 1 deletion src/commands/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function registerOpenCommand(program: Command, getCtx: () => ArbContext):
const { wsDir } = requireWorkspace(ctx);

// Check if command exists in PATH
const which = Bun.spawnSync(["which", command]);
const which = Bun.spawnSync(["which", command], { cwd: wsDir });
if (which.exitCode !== 0) {
error(`'${command}' not found in PATH`);
process.exit(1);
Expand All @@ -48,6 +48,7 @@ export function registerOpenCommand(program: Command, getCtx: () => ArbContext):
}

const proc = Bun.spawn([command, ...extraFlags, ...dirsToOpen], {
cwd: wsDir,
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
Expand Down
14 changes: 10 additions & 4 deletions src/commands/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ export function registerPullCommand(program: Command, getCtx: () => ArbContext):
inlineStart(a.repo, `pulling (${a.pullMode})`);
const pullRemote = remotesMap.get(a.repo)?.publish ?? "origin";
const pullFlag = a.pullMode === "rebase" ? "--rebase" : "--no-rebase";
const pullResult = await Bun.$`git -C ${a.repoDir} pull ${pullFlag} ${pullRemote} ${branch}`.quiet().nothrow();
const pullResult = await Bun.$`git -C ${a.repoDir} pull ${pullFlag} ${pullRemote} ${branch}`
.cwd(a.repoDir)
.quiet()
.nothrow();
if (pullResult.exitCode === 0) {
inlineResult(a.repo, `pulled ${plural(a.behind, "commit")} (${a.pullMode})`);
pullOk++;
Expand Down Expand Up @@ -182,7 +185,7 @@ async function assessPullRepo(
}

if (!(await remoteBranchExists(repoDir, branch, publishRemote))) {
const configRemote = await Bun.$`git -C ${repoDir} config branch.${branch}.remote`.quiet().nothrow();
const configRemote = await Bun.$`git -C ${repoDir} config branch.${branch}.remote`.cwd(repoDir).quiet().nothrow();
if (configRemote.exitCode === 0 && configRemote.text().trim().length > 0) {
return { ...base, skipReason: "remote branch gone" };
}
Expand All @@ -208,11 +211,14 @@ async function assessPullRepo(
}

async function detectPullMode(repoDir: string, branch: string): Promise<"rebase" | "merge"> {
const branchRebase = await Bun.$`git -C ${repoDir} config --get branch.${branch}.rebase`.quiet().nothrow();
const branchRebase = await Bun.$`git -C ${repoDir} config --get branch.${branch}.rebase`
.cwd(repoDir)
.quiet()
.nothrow();
if (branchRebase.exitCode === 0) {
return branchRebase.text().trim() !== "false" ? "rebase" : "merge";
}
const pullRebase = await Bun.$`git -C ${repoDir} config --get pull.rebase`.quiet().nothrow();
const pullRebase = await Bun.$`git -C ${repoDir} config --get pull.rebase`.cwd(repoDir).quiet().nothrow();
if (pullRebase.exitCode === 0) {
return pullRebase.text().trim() !== "false" ? "rebase" : "merge";
}
Expand Down
4 changes: 2 additions & 2 deletions src/commands/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function registerPushCommand(program: Command, getCtx: () => ArbContext):
a.outcome === "will-force-push"
? ["push", "-u", "--force-with-lease", a.publishRemote, a.branch]
: ["push", "-u", a.publishRemote, a.branch];
const pushResult = await Bun.$`git -C ${a.repoDir} ${pushArgs}`.quiet().nothrow();
const pushResult = await Bun.$`git -C ${a.repoDir} ${pushArgs}`.cwd(a.repoDir).quiet().nothrow();
if (pushResult.exitCode === 0) {
inlineResult(a.repo, `pushed ${plural(a.ahead, "commit")}`);
pushOk++;
Expand Down Expand Up @@ -171,7 +171,7 @@ async function assessPushRepo(
if (!(await remoteBranchExists(repoDir, branch, publishRemote))) {
// Tracking config present means the branch was pushed before (set by git push -u).
// If it's gone now, the remote branch was deleted (e.g. merged via PR).
const trackingRemote = await Bun.$`git -C ${repoDir} config branch.${branch}.remote`.quiet().nothrow();
const trackingRemote = await Bun.$`git -C ${repoDir} config branch.${branch}.remote`.cwd(repoDir).quiet().nothrow();
if (trackingRemote.exitCode === 0 && trackingRemote.text().trim()) {
return { ...base, skipReason: "remote branch gone" };
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function detectOperation(repoDir: string): Promise<GitOperation> {

export async function git(repoDir: string, ...args: string[]): Promise<{ exitCode: number; stdout: string }> {
const proc = Bun.spawn(["git", "-C", repoDir, ...args], {
cwd: repoDir,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
Expand Down
3 changes: 2 additions & 1 deletion src/lib/parallel-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function parallelFetch(

for (const remote of remotesToFetch) {
const proc = Bun.spawn(["git", "-C", repoDir, "fetch", "--prune", remote], {
cwd: repoDir,
stdout: "pipe",
stderr: "pipe",
});
Expand Down Expand Up @@ -87,7 +88,7 @@ export async function parallelFetch(
results.set(repo, { repo, exitCode: lastExitCode, output: allOutput });

// Auto-detect remote HEAD on the upstream remote
await Bun.$`git -C ${repoDir} remote set-head ${upstreamRemote} --auto`.quiet().nothrow();
await Bun.$`git -C ${repoDir} remote set-head ${upstreamRemote} --auto`.cwd(repoDir).quiet().nothrow();
} catch {
results.set(repo, { repo, exitCode: 1, output: "fetch failed" });
}
Expand Down
5 changes: 3 additions & 2 deletions src/lib/workspace-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ export async function workspaceBranch(wsDir: string): Promise<WorkspaceBranchRes

// Config missing or empty — try to infer from first worktree
const repoDirs = workspaceRepoDirs(wsDir);
if (repoDirs.length > 0) {
const result = await Bun.$`git -C ${repoDirs[0]} branch --show-current`.quiet().nothrow();
const firstRepoDir = repoDirs[0];
if (firstRepoDir) {
const result = await Bun.$`git -C ${firstRepoDir} branch --show-current`.cwd(firstRepoDir).quiet().nothrow();
if (result.exitCode === 0) {
const branch = result.text().trim();
if (branch) {
Expand Down
8 changes: 6 additions & 2 deletions src/lib/worktrees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,14 @@ export async function addWorktrees(
const branchExists = await branchExistsLocally(repoPath, branch);

// Prune stale worktrees
await Bun.$`git -C ${repoPath} worktree prune`.quiet().nothrow();
await Bun.$`git -C ${repoPath} worktree prune`.cwd(repoPath).quiet().nothrow();

if (branchExists) {
inlineStart(repo, `attaching branch ${branch}`);
const wt = await Bun.$`git -C ${repoPath} worktree add ${wsDir}/${repo} ${branch}`.quiet().nothrow();
const wt = await Bun.$`git -C ${repoPath} worktree add ${wsDir}/${repo} ${branch}`
.cwd(repoPath)
.quiet()
.nothrow();
if (wt.exitCode !== 0) {
inlineResult(repo, "failed");
const errText = wt.stderr.toString().trim();
Expand All @@ -142,6 +145,7 @@ export async function addWorktrees(
// (pushed, merged, remote branch deleted) vs never-pushed branches.
const noTrack = repoHasRemote ? ["--no-track"] : [];
const wt = await Bun.$`git -C ${repoPath} worktree add ${noTrack} -b ${branch} ${wsDir}/${repo} ${startPoint}`
.cwd(repoPath)
.quiet()
.nothrow();
if (wt.exitCode !== 0) {
Expand Down
10 changes: 10 additions & 0 deletions test/arb.bats
Original file line number Diff line number Diff line change
Expand Up @@ -3626,3 +3626,13 @@ push_then_delete_remote() {
[ ! -d "$TEST_DIR/project/tpl-allok" ]
}

@test "arb remove --force succeeds when cwd is inside the workspace being removed" {
arb create doomed repo-a repo-b

cd "$TEST_DIR/project/doomed"
run arb remove doomed --force
[ "$status" -eq 0 ]
[[ "$output" == *"Removed workspace doomed"* ]]
[ ! -d "$TEST_DIR/project/doomed" ]
}

Loading