Skip to content

Commit 315ed8d

Browse files
committed
🤖 feat: always pull latest when creating workspaces
Automatically fetch and rebase on origin/<trunk> when creating new workspaces. This ensures workspaces start with the latest changes. Backend changes: - LocalRuntime: fetch and rebase after worktree creation - SSHRuntime: fetch and rebase after branch checkout Error handling is best-effort - the init log informs users of status: - Success: 'Rebased on latest origin successfully' - Fetch fail: 'Note: Could not fetch from origin (...), using local branch state' - Rebase fail: 'Note: Rebase skipped (...), using local branch state' Failures never block workspace creation. _Generated with `mux`_
1 parent 27c9cd0 commit 315ed8d

File tree

2 files changed

+119
-1
lines changed

2 files changed

+119
-1
lines changed

src/node/runtime/LocalRuntime.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,9 @@ export class LocalRuntime implements Runtime {
365365

366366
initLogger.logStep("Worktree created successfully");
367367

368+
// Pull latest from origin (best-effort, non-blocking on failure)
369+
await this.pullLatestFromOrigin(workspacePath, trunkBranch, initLogger);
370+
368371
return { success: true, workspacePath };
369372
} catch (error) {
370373
return {
@@ -374,6 +377,49 @@ export class LocalRuntime implements Runtime {
374377
}
375378
}
376379

380+
/**
381+
* Fetch and rebase on latest origin/<trunkBranch>
382+
* Best-effort operation - logs status but doesn't fail workspace creation
383+
*/
384+
private async pullLatestFromOrigin(
385+
workspacePath: string,
386+
trunkBranch: string,
387+
initLogger: InitLogger
388+
): Promise<void> {
389+
try {
390+
initLogger.logStep(`Fetching latest from origin/${trunkBranch}...`);
391+
392+
// Fetch the trunk branch from origin
393+
using fetchProc = execAsync(`git -C "${workspacePath}" fetch origin "${trunkBranch}"`);
394+
await fetchProc.result;
395+
396+
initLogger.logStep("Rebasing on latest...");
397+
398+
// Attempt rebase on origin/<trunkBranch>
399+
try {
400+
using rebaseProc = execAsync(`git -C "${workspacePath}" rebase "origin/${trunkBranch}"`);
401+
await rebaseProc.result;
402+
initLogger.logStep("Rebased on latest origin successfully");
403+
} catch (rebaseError) {
404+
// Rebase failed (conflicts or other error) - abort and warn
405+
try {
406+
using abortProc = execAsync(`git -C "${workspacePath}" rebase --abort`);
407+
await abortProc.result;
408+
} catch {
409+
// Abort may fail if rebase wasn't in progress; ignore
410+
}
411+
const errorMsg = getErrorMessage(rebaseError);
412+
initLogger.logStderr(`Note: Rebase skipped (${errorMsg}), using local branch state`);
413+
}
414+
} catch (error) {
415+
// Fetch failed - log and continue (common for repos without remote)
416+
const errorMsg = getErrorMessage(error);
417+
initLogger.logStderr(
418+
`Note: Could not fetch from origin (${errorMsg}), using local branch state`
419+
);
420+
}
421+
}
422+
377423
async initWorkspace(params: WorkspaceInitParams): Promise<WorkspaceInitResult> {
378424
const { projectPath, workspacePath, initLogger } = params;
379425

src/node/runtime/SSHRuntime.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,10 @@ export class SSHRuntime implements Runtime {
906906
}
907907
initLogger.logStep("Branch checked out successfully");
908908

909-
// 3. Run .mux/init hook if it exists
909+
// 3. Pull latest from origin (best-effort, non-blocking on failure)
910+
await this.pullLatestFromOrigin(workspacePath, trunkBranch, initLogger, abortSignal);
911+
912+
// 4. Run .mux/init hook if it exists
910913
// Note: runInitHook calls logComplete() internally if hook exists
911914
const hookExists = await checkInitHookExists(projectPath);
912915
if (hookExists) {
@@ -928,6 +931,75 @@ export class SSHRuntime implements Runtime {
928931
}
929932
}
930933

934+
/**
935+
* Fetch and rebase on latest origin/<trunkBranch> on remote
936+
* Best-effort operation - logs status but doesn't fail workspace initialization
937+
*/
938+
private async pullLatestFromOrigin(
939+
workspacePath: string,
940+
trunkBranch: string,
941+
initLogger: InitLogger,
942+
abortSignal?: AbortSignal
943+
): Promise<void> {
944+
try {
945+
initLogger.logStep(`Fetching latest from origin/${trunkBranch}...`);
946+
947+
// Fetch the trunk branch from origin
948+
const fetchCmd = `git fetch origin ${shescape.quote(trunkBranch)}`;
949+
const fetchStream = await this.exec(fetchCmd, {
950+
cwd: workspacePath,
951+
timeout: 120, // 2 minutes for network operation
952+
abortSignal,
953+
});
954+
955+
const fetchExitCode = await fetchStream.exitCode;
956+
if (fetchExitCode !== 0) {
957+
const fetchStderr = await streamToString(fetchStream.stderr);
958+
initLogger.logStderr(
959+
`Note: Could not fetch from origin (${fetchStderr}), using local branch state`
960+
);
961+
return;
962+
}
963+
964+
initLogger.logStep("Rebasing on latest...");
965+
966+
// Attempt rebase on origin/<trunkBranch>
967+
const rebaseCmd = `git rebase origin/${shescape.quote(trunkBranch)}`;
968+
const rebaseStream = await this.exec(rebaseCmd, {
969+
cwd: workspacePath,
970+
timeout: 300, // 5 minutes for rebase
971+
abortSignal,
972+
});
973+
974+
const [rebaseStderr, rebaseExitCode] = await Promise.all([
975+
streamToString(rebaseStream.stderr),
976+
rebaseStream.exitCode,
977+
]);
978+
979+
if (rebaseExitCode !== 0 || rebaseStderr.includes("CONFLICT")) {
980+
// Rebase has conflicts or failed - abort and warn
981+
const abortCmd = "git rebase --abort";
982+
const abortStream = await this.exec(abortCmd, {
983+
cwd: workspacePath,
984+
timeout: 30,
985+
abortSignal,
986+
});
987+
await abortStream.exitCode;
988+
initLogger.logStderr(
989+
`Note: Rebase skipped (${rebaseStderr || "conflicts"}), using local branch state`
990+
);
991+
} else {
992+
initLogger.logStep("Rebased on latest origin successfully");
993+
}
994+
} catch (error) {
995+
// Non-fatal: log and continue
996+
const errorMsg = getErrorMessage(error);
997+
initLogger.logStderr(
998+
`Note: Could not fetch from origin (${errorMsg}), using local branch state`
999+
);
1000+
}
1001+
}
1002+
9311003
async renameWorkspace(
9321004
projectPath: string,
9331005
oldName: string,

0 commit comments

Comments
 (0)