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
42 changes: 42 additions & 0 deletions src/node/runtime/LocalRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ export class LocalRuntime implements Runtime {

initLogger.logStep("Worktree created successfully");

// Pull latest from origin (best-effort, non-blocking on failure)
await this.pullLatestFromOrigin(workspacePath, trunkBranch, initLogger);

return { success: true, workspacePath };
} catch (error) {
return {
Expand All @@ -374,6 +377,45 @@ export class LocalRuntime implements Runtime {
}
}

/**
* Fetch and rebase on latest origin/<trunkBranch>
* Best-effort operation - logs status but doesn't fail workspace creation
*/
private async pullLatestFromOrigin(
workspacePath: string,
trunkBranch: string,
initLogger: InitLogger
): Promise<void> {
try {
initLogger.logStep(`Fetching latest from origin/${trunkBranch}...`);

// Fetch the trunk branch from origin
using fetchProc = execAsync(`git -C "${workspacePath}" fetch origin "${trunkBranch}"`);
await fetchProc.result;

initLogger.logStep("Fast-forward merging...");

// Attempt fast-forward merge from origin/<trunkBranch>
try {
using mergeProc = execAsync(
`git -C "${workspacePath}" merge --ff-only "origin/${trunkBranch}"`
);
await mergeProc.result;
initLogger.logStep("Fast-forwarded to latest origin successfully");
} catch (mergeError) {
// Fast-forward not possible (diverged branches) - just warn
const errorMsg = getErrorMessage(mergeError);
initLogger.logStderr(`Note: Fast-forward skipped (${errorMsg}), using local branch state`);
}
} catch (error) {
// Fetch failed - log and continue (common for repos without remote)
const errorMsg = getErrorMessage(error);
initLogger.logStderr(
`Note: Could not fetch from origin (${errorMsg}), using local branch state`
);
}
}

async initWorkspace(params: WorkspaceInitParams): Promise<WorkspaceInitResult> {
const { projectPath, workspacePath, initLogger } = params;

Expand Down
67 changes: 66 additions & 1 deletion src/node/runtime/SSHRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,10 @@ export class SSHRuntime implements Runtime {
}
initLogger.logStep("Branch checked out successfully");

// 3. Run .mux/init hook if it exists
// 3. Pull latest from origin (best-effort, non-blocking on failure)
await this.pullLatestFromOrigin(workspacePath, trunkBranch, initLogger, abortSignal);

// 4. Run .mux/init hook if it exists
// Note: runInitHook calls logComplete() internally if hook exists
const hookExists = await checkInitHookExists(projectPath);
if (hookExists) {
Expand All @@ -928,6 +931,68 @@ export class SSHRuntime implements Runtime {
}
}

/**
* Fetch and rebase on latest origin/<trunkBranch> on remote
* Best-effort operation - logs status but doesn't fail workspace initialization
*/
private async pullLatestFromOrigin(
workspacePath: string,
trunkBranch: string,
initLogger: InitLogger,
abortSignal?: AbortSignal
): Promise<void> {
try {
initLogger.logStep(`Fetching latest from origin/${trunkBranch}...`);

// Fetch the trunk branch from origin
const fetchCmd = `git fetch origin ${shescape.quote(trunkBranch)}`;
const fetchStream = await this.exec(fetchCmd, {
cwd: workspacePath,
timeout: 120, // 2 minutes for network operation
abortSignal,
});

const fetchExitCode = await fetchStream.exitCode;
if (fetchExitCode !== 0) {
const fetchStderr = await streamToString(fetchStream.stderr);
initLogger.logStderr(
`Note: Could not fetch from origin (${fetchStderr}), using local branch state`
);
return;
}

initLogger.logStep("Fast-forward merging...");

// Attempt fast-forward merge from origin/<trunkBranch>
const mergeCmd = `git merge --ff-only origin/${shescape.quote(trunkBranch)}`;
const mergeStream = await this.exec(mergeCmd, {
cwd: workspacePath,
timeout: 60, // 1 minute for fast-forward merge
abortSignal,
});

const [mergeStderr, mergeExitCode] = await Promise.all([
streamToString(mergeStream.stderr),
mergeStream.exitCode,
]);

if (mergeExitCode !== 0) {
// Fast-forward not possible (diverged branches) - just warn
initLogger.logStderr(
`Note: Fast-forward skipped (${mergeStderr || "branches diverged"}), using local branch state`
);
} else {
initLogger.logStep("Fast-forwarded to latest origin successfully");
}
} catch (error) {
// Non-fatal: log and continue
const errorMsg = getErrorMessage(error);
initLogger.logStderr(
`Note: Could not fetch from origin (${errorMsg}), using local branch state`
);
}
}

async renameWorkspace(
projectPath: string,
oldName: string,
Expand Down
6 changes: 4 additions & 2 deletions tests/ipcMain/initWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ describeIntegration("IpcMain workspace init hook integration tests", () => {
expect(outputLines).toContain("Installing dependencies...");
expect(outputLines).toContain("Build complete!");

expect(errorEvents.length).toBe(1);
expect(errorEvents[0].line).toBe("Warning: deprecated package");
// Should have at least the hook's stderr message
// (may also have pull-latest notes if fetch/rebase fails, which is expected)
const hookErrorEvent = errorEvents.find((e) => e.line === "Warning: deprecated package");
expect(hookErrorEvent).toBeDefined();

// Last event should be end with exitCode 0
const finalEvent = initEvents[initEvents.length - 1];
Expand Down