mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 10:43:08 +00:00
Feature: Git sync, set-tracking, and push divergence handling (#796)
This commit is contained in:
@@ -67,6 +67,8 @@ import { createAbortOperationHandler } from './routes/abort-operation.js';
|
||||
import { createContinueOperationHandler } from './routes/continue-operation.js';
|
||||
import { createStageFilesHandler } from './routes/stage-files.js';
|
||||
import { createCheckChangesHandler } from './routes/check-changes.js';
|
||||
import { createSetTrackingHandler } from './routes/set-tracking.js';
|
||||
import { createSyncHandler } from './routes/sync.js';
|
||||
import type { SettingsService } from '../../services/settings-service.js';
|
||||
|
||||
export function createWorktreeRoutes(
|
||||
@@ -118,6 +120,18 @@ export function createWorktreeRoutes(
|
||||
requireValidWorktree,
|
||||
createPullHandler()
|
||||
);
|
||||
router.post(
|
||||
'/sync',
|
||||
validatePathParams('worktreePath'),
|
||||
requireValidWorktree,
|
||||
createSyncHandler()
|
||||
);
|
||||
router.post(
|
||||
'/set-tracking',
|
||||
validatePathParams('worktreePath'),
|
||||
requireValidWorktree,
|
||||
createSetTrackingHandler()
|
||||
);
|
||||
router.post(
|
||||
'/checkout-branch',
|
||||
validatePathParams('worktreePath'),
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
/**
|
||||
* POST /push endpoint - Push a worktree branch to remote
|
||||
*
|
||||
* Git business logic is delegated to push-service.ts.
|
||||
*
|
||||
* Note: Git repository validation (isGitRepo, hasCommits) is handled by
|
||||
* the requireValidWorktree middleware in index.ts
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
import { performPush } from '../../../services/push-service.js';
|
||||
|
||||
export function createPushHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { worktreePath, force, remote } = req.body as {
|
||||
const { worktreePath, force, remote, autoResolve } = req.body as {
|
||||
worktreePath: string;
|
||||
force?: boolean;
|
||||
remote?: string;
|
||||
autoResolve?: boolean;
|
||||
};
|
||||
|
||||
if (!worktreePath) {
|
||||
@@ -29,34 +29,28 @@ export function createPushHandler() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get branch name
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
const result = await performPush(worktreePath, { remote, force, autoResolve });
|
||||
|
||||
// Use specified remote or default to 'origin'
|
||||
const targetRemote = remote || 'origin';
|
||||
|
||||
// Push the branch
|
||||
const forceFlag = force ? '--force' : '';
|
||||
try {
|
||||
await execAsync(`git push -u ${targetRemote} ${branchName} ${forceFlag}`, {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
} catch {
|
||||
// Try setting upstream
|
||||
await execAsync(`git push --set-upstream ${targetRemote} ${branchName} ${forceFlag}`, {
|
||||
cwd: worktreePath,
|
||||
if (!result.success) {
|
||||
const statusCode = isClientError(result.error ?? '') ? 400 : 500;
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: result.error,
|
||||
diverged: result.diverged,
|
||||
hasConflicts: result.hasConflicts,
|
||||
conflictFiles: result.conflictFiles,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
branch: branchName,
|
||||
pushed: true,
|
||||
message: `Successfully pushed ${branchName} to ${targetRemote}`,
|
||||
branch: result.branch,
|
||||
pushed: result.pushed,
|
||||
diverged: result.diverged,
|
||||
autoResolved: result.autoResolved,
|
||||
message: result.message,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -65,3 +59,15 @@ export function createPushHandler() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an error message represents a client error (400)
|
||||
* vs a server error (500).
|
||||
*/
|
||||
function isClientError(errorMessage: string): boolean {
|
||||
return (
|
||||
errorMessage.includes('detached HEAD') ||
|
||||
errorMessage.includes('rejected') ||
|
||||
errorMessage.includes('diverged')
|
||||
);
|
||||
}
|
||||
|
||||
76
apps/server/src/routes/worktree/routes/set-tracking.ts
Normal file
76
apps/server/src/routes/worktree/routes/set-tracking.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* POST /set-tracking endpoint - Set the upstream tracking branch for a worktree
|
||||
*
|
||||
* Sets `git branch --set-upstream-to=<remote>/<branch>` for the current branch.
|
||||
*
|
||||
* Note: Git repository validation (isGitRepo, hasCommits) is handled by
|
||||
* the requireValidWorktree middleware in index.ts
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { execGitCommand } from '@automaker/git-utils';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { getCurrentBranch } from '../../../lib/git.js';
|
||||
|
||||
export function createSetTrackingHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { worktreePath, remote, branch } = req.body as {
|
||||
worktreePath: string;
|
||||
remote: string;
|
||||
branch?: string;
|
||||
};
|
||||
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({ success: false, error: 'worktreePath required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!remote) {
|
||||
res.status(400).json({ success: false, error: 'remote required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current branch if not provided
|
||||
let targetBranch = branch;
|
||||
if (!targetBranch) {
|
||||
try {
|
||||
targetBranch = await getCurrentBranch(worktreePath);
|
||||
} catch (err) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Failed to get current branch: ${getErrorMessage(err)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetBranch === 'HEAD') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Cannot set tracking in detached HEAD state.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Set upstream tracking (pass local branch name as final arg to be explicit)
|
||||
await execGitCommand(
|
||||
['branch', '--set-upstream-to', `${remote}/${targetBranch}`, targetBranch],
|
||||
worktreePath
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
branch: targetBranch,
|
||||
remote,
|
||||
upstream: `${remote}/${targetBranch}`,
|
||||
message: `Set tracking branch to ${remote}/${targetBranch}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Set tracking branch failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
66
apps/server/src/routes/worktree/routes/sync.ts
Normal file
66
apps/server/src/routes/worktree/routes/sync.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* POST /sync endpoint - Pull then push a worktree branch
|
||||
*
|
||||
* Performs a full sync operation: pull latest from remote, then push
|
||||
* local commits. Handles divergence automatically.
|
||||
*
|
||||
* Git business logic is delegated to sync-service.ts.
|
||||
*
|
||||
* Note: Git repository validation (isGitRepo, hasCommits) is handled by
|
||||
* the requireValidWorktree middleware in index.ts
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { performSync } from '../../../services/sync-service.js';
|
||||
|
||||
export function createSyncHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { worktreePath, remote } = req.body as {
|
||||
worktreePath: string;
|
||||
remote?: string;
|
||||
};
|
||||
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await performSync(worktreePath, { remote });
|
||||
|
||||
if (!result.success) {
|
||||
const statusCode = result.hasConflicts ? 409 : 500;
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: result.error,
|
||||
hasConflicts: result.hasConflicts,
|
||||
conflictFiles: result.conflictFiles,
|
||||
conflictSource: result.conflictSource,
|
||||
pulled: result.pulled,
|
||||
pushed: result.pushed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
branch: result.branch,
|
||||
pulled: result.pulled,
|
||||
pushed: result.pushed,
|
||||
isFastForward: result.isFastForward,
|
||||
isMerge: result.isMerge,
|
||||
autoResolved: result.autoResolved,
|
||||
message: result.message,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, 'Sync worktree failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user