Files
openchamber/packages/web/server/lib/git/routes.js
T
Bohdan Triapitsyn 93267927ff feat: add git stash management
Add a Stashes dialog with create, apply, pop, and drop actions
Include untracked files automatically when stashing
Show file counts for current changes and stash entries
2026-05-05 23:39:20 +03:00

947 lines
33 KiB
JavaScript

export function registerGitRoutes(app) {
let gitLibraries = null;
const getGitLibraries = async () => {
if (!gitLibraries) {
gitLibraries = await import('./index.js');
}
return gitLibraries;
};
app.get('/api/git/identities', async (req, res) => {
const { getProfiles } = await getGitLibraries();
try {
const profiles = getProfiles();
res.json(profiles);
} catch (error) {
console.error('Failed to list git identity profiles:', error);
res.status(500).json({ error: 'Failed to list git identity profiles' });
}
});
app.post('/api/git/identities', async (req, res) => {
const { createProfile } = await getGitLibraries();
try {
const profile = createProfile(req.body);
console.log(`Created git identity profile: ${profile.name} (${profile.id})`);
res.json(profile);
} catch (error) {
console.error('Failed to create git identity profile:', error);
res.status(400).json({ error: error.message || 'Failed to create git identity profile' });
}
});
app.put('/api/git/identities/:id', async (req, res) => {
const { updateProfile } = await getGitLibraries();
try {
const profile = updateProfile(req.params.id, req.body);
console.log(`Updated git identity profile: ${profile.name} (${profile.id})`);
res.json(profile);
} catch (error) {
console.error('Failed to update git identity profile:', error);
res.status(400).json({ error: error.message || 'Failed to update git identity profile' });
}
});
app.delete('/api/git/identities/:id', async (req, res) => {
const { deleteProfile } = await getGitLibraries();
try {
deleteProfile(req.params.id);
console.log(`Deleted git identity profile: ${req.params.id}`);
res.json({ success: true });
} catch (error) {
console.error('Failed to delete git identity profile:', error);
res.status(400).json({ error: error.message || 'Failed to delete git identity profile' });
}
});
app.get('/api/git/global-identity', async (req, res) => {
const { getGlobalIdentity } = await getGitLibraries();
try {
const identity = await getGlobalIdentity();
res.json(identity);
} catch (error) {
console.error('Failed to get global git identity:', error);
res.status(500).json({ error: 'Failed to get global git identity' });
}
});
app.get('/api/git/discover-credentials', async (req, res) => {
try {
const { discoverGitCredentials } = await import('./index.js');
const credentials = discoverGitCredentials();
res.json(credentials);
} catch (error) {
console.error('Failed to discover git credentials:', error);
res.status(500).json({ error: 'Failed to discover git credentials' });
}
});
app.get('/api/git/check', async (req, res) => {
const { isGitRepository } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const isRepo = await isGitRepository(directory);
res.json({ isGitRepository: isRepo });
} catch (error) {
console.error('Failed to check git repository:', error);
res.status(500).json({ error: 'Failed to check git repository' });
}
});
app.get('/api/git/remote-url', async (req, res) => {
const { getRemoteUrl } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const remote = req.query.remote || 'origin';
const url = await getRemoteUrl(directory, remote);
res.json({ url });
} catch (error) {
console.error('Failed to get remote url:', error);
res.status(500).json({ error: 'Failed to get remote url' });
}
});
app.get('/api/git/current-identity', async (req, res) => {
const { getCurrentIdentity } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const identity = await getCurrentIdentity(directory);
res.json(identity);
} catch (error) {
console.error('Failed to get current git identity:', error);
res.status(500).json({ error: 'Failed to get current git identity' });
}
});
app.get('/api/git/has-local-identity', async (req, res) => {
const { hasLocalIdentity } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const hasLocal = await hasLocalIdentity(directory);
res.json({ hasLocalIdentity: hasLocal });
} catch (error) {
console.error('Failed to check local git identity:', error);
res.status(500).json({ error: 'Failed to check local git identity' });
}
});
app.post('/api/git/set-identity', async (req, res) => {
const { getProfile, setLocalIdentity, getGlobalIdentity } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { profileId } = req.body;
if (!profileId) {
return res.status(400).json({ error: 'profileId is required' });
}
let profile = null;
if (profileId === 'global') {
const globalIdentity = await getGlobalIdentity();
if (!globalIdentity?.userName || !globalIdentity?.userEmail) {
return res.status(404).json({ error: 'Global identity is not configured' });
}
profile = {
id: 'global',
name: 'Global Identity',
userName: globalIdentity.userName,
userEmail: globalIdentity.userEmail,
sshKey: globalIdentity.sshCommand
? globalIdentity.sshCommand.replace('ssh -i ', '')
: null,
};
} else {
profile = getProfile(profileId);
if (!profile) {
return res.status(404).json({ error: 'Profile not found' });
}
}
await setLocalIdentity(directory, profile);
res.json({ success: true, profile });
} catch (error) {
console.error('Failed to set git identity:', error);
res.status(500).json({ error: error.message || 'Failed to set git identity' });
}
});
app.get('/api/git/status', async (req, res) => {
const { getStatus, isGitRepository } = await getGitLibraries();
const extractGitErrorText = (error) => {
const message = typeof error?.message === 'string' ? error.message : '';
const stderr = typeof error?.stderr === 'string' ? error.stderr : '';
const stdout = typeof error?.stdout === 'string' ? error.stdout : '';
return [message, stderr, stdout]
.map((value) => String(value || '').trim())
.filter(Boolean)
.join('\n');
};
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const isRepo = await isGitRepository(directory);
if (!isRepo) {
return res.json({ isGitRepository: false, files: [], branch: null, ahead: 0, behind: 0 });
}
const mode = req.query.mode === 'light' ? 'light' : undefined;
const status = await getStatus(directory, { mode });
res.json(status);
} catch (error) {
const errorText = extractGitErrorText(error);
if (/not a git repository/i.test(errorText)) {
return res.json({ isGitRepository: false, files: [], branch: null, ahead: 0, behind: 0 });
}
console.error('Failed to get git status:', error);
res.status(500).json({ error: error.message || 'Failed to get git status' });
}
});
app.get('/api/git/diff', async (req, res) => {
const { getDiff } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const path = req.query.path;
if (!path || typeof path !== 'string') {
return res.status(400).json({ error: 'path parameter is required' });
}
const staged = req.query.staged === 'true';
const context = req.query.context ? parseInt(String(req.query.context), 10) : undefined;
const diff = await getDiff(directory, {
path,
staged,
contextLines: Number.isFinite(context) ? context : 3,
});
res.json({ diff });
} catch (error) {
console.error('Failed to get git diff:', error);
res.status(500).json({ error: error.message || 'Failed to get git diff' });
}
});
app.get('/api/git/file-diff', async (req, res) => {
const { getFileDiff } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory parameter is required' });
}
const pathParam = req.query.path;
if (!pathParam || typeof pathParam !== 'string') {
return res.status(400).json({ error: 'path parameter is required' });
}
const staged = req.query.staged === 'true';
const result = await getFileDiff(directory, {
path: pathParam,
staged,
});
res.json({
original: result.original,
modified: result.modified,
path: result.path,
isBinary: Boolean(result.isBinary),
});
} catch (error) {
console.error('Failed to get git file diff:', error);
res.status(500).json({ error: error.message || 'Failed to get git file diff' });
}
});
app.post('/api/git/revert', async (req, res) => {
const { revertFile } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { path } = req.body || {};
if (!path || typeof path !== 'string') {
return res.status(400).json({ error: 'path parameter is required' });
}
await revertFile(directory, path);
res.json({ success: true });
} catch (error) {
console.error('Failed to revert git file:', error);
res.status(500).json({ error: error.message || 'Failed to revert git file' });
}
});
app.post('/api/git/pull', async (req, res) => {
const { pull } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await pull(directory, req.body);
res.json(result);
} catch (error) {
console.error('Failed to pull:', error);
res.status(500).json({ error: error.message || 'Failed to pull from remote' });
}
});
app.post('/api/git/push', async (req, res) => {
const { push } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await push(directory, req.body);
res.json(result);
} catch (error) {
console.error('Failed to push:', error);
res.status(500).json({ error: error.message || 'Failed to push to remote' });
}
});
app.get('/api/git/stashes', async (req, res) => {
const { listStashes } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
res.json({ stashes: await listStashes(directory) });
} catch (error) {
console.error('Failed to list stashes:', error);
res.status(500).json({ error: error.message || 'Failed to list stashes' });
}
});
app.post('/api/git/stashes/file-counts', async (req, res) => {
const { countStashFiles } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
res.json({ counts: await countStashFiles(directory, req.body?.refs) });
} catch (error) {
console.error('Failed to count stash files:', error);
res.status(500).json({ error: error.message || 'Failed to count stash files' });
}
});
app.post('/api/git/stash', async (req, res) => {
const { stashPush } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
res.json(await stashPush(directory, req.body));
} catch (error) {
console.error('Failed to stash changes:', error);
res.status(500).json({ error: error.message || 'Failed to stash changes' });
}
});
app.post('/api/git/stash/apply', async (req, res) => {
const { stashApply } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
res.json(await stashApply(directory, req.body));
} catch (error) {
console.error('Failed to apply stash:', error);
res.status(500).json({ error: error.message || 'Failed to apply stash' });
}
});
app.post('/api/git/stash/pop', async (req, res) => {
const { stashPop } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
res.json(await stashPop(directory, req.body));
} catch (error) {
console.error('Failed to pop stash:', error);
res.status(500).json({ error: error.message || 'Failed to pop stash' });
}
});
app.post('/api/git/stash/drop', async (req, res) => {
const { stashDrop } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) return res.status(400).json({ error: 'directory parameter is required' });
res.json(await stashDrop(directory, req.body));
} catch (error) {
console.error('Failed to drop stash:', error);
res.status(500).json({ error: error.message || 'Failed to drop stash' });
}
});
app.post('/api/git/fetch', async (req, res) => {
const { fetch: gitFetch } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await gitFetch(directory, req.body);
res.json(result);
} catch (error) {
console.error('Failed to fetch:', error);
res.status(500).json({ error: error.message || 'Failed to fetch from remote' });
}
});
app.get('/api/git/remotes', async (req, res) => {
const { getRemotes } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const remotes = await getRemotes(directory);
res.json(remotes);
} catch (error) {
console.error('Failed to get remotes:', error);
res.status(500).json({ error: error.message || 'Failed to get remotes' });
}
});
app.delete('/api/git/remotes', async (req, res) => {
const { removeRemote } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const remote = String(req.body?.remote || '').trim();
if (!remote) {
return res.status(400).json({ error: 'remote is required' });
}
const result = await removeRemote(directory, { remote });
res.json(result);
} catch (error) {
console.error('Failed to remove remote:', error);
res.status(500).json({ error: error.message || 'Failed to remove remote' });
}
});
app.post('/api/git/rebase', async (req, res) => {
const { rebase } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await rebase(directory, req.body);
res.json(result);
} catch (error) {
console.error('Failed to rebase:', error);
res.status(500).json({ error: error.message || 'Failed to rebase' });
}
});
app.post('/api/git/rebase/abort', async (req, res) => {
const { abortRebase } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await abortRebase(directory);
res.json(result);
} catch (error) {
console.error('Failed to abort rebase:', error);
res.status(500).json({ error: error.message || 'Failed to abort rebase' });
}
});
app.post('/api/git/merge', async (req, res) => {
const { merge } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await merge(directory, req.body);
res.json(result);
} catch (error) {
console.error('Failed to merge:', error);
res.status(500).json({ error: error.message || 'Failed to merge' });
}
});
app.post('/api/git/merge/abort', async (req, res) => {
const { abortMerge } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await abortMerge(directory);
res.json(result);
} catch (error) {
console.error('Failed to abort merge:', error);
res.status(500).json({ error: error.message || 'Failed to abort merge' });
}
});
app.post('/api/git/rebase/continue', async (req, res) => {
const { continueRebase } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await continueRebase(directory);
res.json(result);
} catch (error) {
console.error('Failed to continue rebase:', error);
res.status(500).json({ error: error.message || 'Failed to continue rebase' });
}
});
app.post('/api/git/merge/continue', async (req, res) => {
const { continueMerge } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await continueMerge(directory);
res.json(result);
} catch (error) {
console.error('Failed to continue merge:', error);
res.status(500).json({ error: error.message || 'Failed to continue merge' });
}
});
app.get('/api/git/conflict-details', async (req, res) => {
const { getConflictDetails } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await getConflictDetails(directory);
res.json(result);
} catch (error) {
console.error('Failed to get conflict details:', error);
res.status(500).json({ error: error.message || 'Failed to get conflict details' });
}
});
app.post('/api/git/commit', async (req, res) => {
const { commit } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { message, addAll, files } = req.body;
if (!message) {
return res.status(400).json({ error: 'message is required' });
}
const result = await commit(directory, message, {
addAll,
files,
});
res.json(result);
} catch (error) {
console.error('Failed to commit:', error);
res.status(500).json({ error: error.message || 'Failed to create commit' });
}
});
app.get('/api/git/branches', async (req, res) => {
const { getBranches } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const branches = await getBranches(directory);
res.json(branches);
} catch (error) {
console.error('Failed to get branches:', error);
res.status(500).json({ error: error.message || 'Failed to get branches' });
}
});
app.post('/api/git/branches', async (req, res) => {
const { createBranch } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { name, startPoint } = req.body;
if (!name) {
return res.status(400).json({ error: 'name is required' });
}
const result = await createBranch(directory, name, { startPoint });
res.json(result);
} catch (error) {
console.error('Failed to create branch:', error);
res.status(500).json({ error: error.message || 'Failed to create branch' });
}
});
app.delete('/api/git/branches', async (req, res) => {
const { deleteBranch } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { branch, force } = req.body;
if (!branch) {
return res.status(400).json({ error: 'branch is required' });
}
const result = await deleteBranch(directory, branch, { force });
res.json(result);
} catch (error) {
console.error('Failed to delete branch:', error);
res.status(500).json({ error: error.message || 'Failed to delete branch' });
}
});
app.put('/api/git/branches/rename', async (req, res) => {
const { renameBranch } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { oldName, newName } = req.body;
if (!oldName) {
return res.status(400).json({ error: 'oldName is required' });
}
if (!newName) {
return res.status(400).json({ error: 'newName is required' });
}
const result = await renameBranch(directory, oldName, newName);
res.json(result);
} catch (error) {
console.error('Failed to rename branch:', error);
res.status(500).json({ error: error.message || 'Failed to rename branch' });
}
});
app.delete('/api/git/remote-branches', async (req, res) => {
const { deleteRemoteBranch } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { branch, remote } = req.body;
if (!branch) {
return res.status(400).json({ error: 'branch is required' });
}
const result = await deleteRemoteBranch(directory, { branch, remote });
res.json(result);
} catch (error) {
console.error('Failed to delete remote branch:', error);
res.status(500).json({ error: error.message || 'Failed to delete remote branch' });
}
});
app.post('/api/git/checkout', async (req, res) => {
const { checkoutBranch } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { branch } = req.body;
if (!branch) {
return res.status(400).json({ error: 'branch is required' });
}
const result = await checkoutBranch(directory, branch);
res.json(result);
} catch (error) {
console.error('Failed to checkout branch:', error);
res.status(500).json({ error: error.message || 'Failed to checkout branch' });
}
});
app.get('/api/git/worktrees', async (req, res) => {
const { getWorktrees } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const worktrees = await getWorktrees(directory);
res.json(worktrees);
} catch (error) {
// Worktrees are an optional feature. Avoid repeated 500s (and repeated client retries)
// when the directory isn't a git repo or uses shell shorthand like "~/".
console.warn('Failed to get worktrees, returning empty list:', error?.message || error);
res.setHeader('X-OpenChamber-Warning', 'git worktrees unavailable');
res.json([]);
}
});
app.post('/api/git/worktrees/validate', async (req, res) => {
const { validateWorktreeCreate } = await getGitLibraries();
if (typeof validateWorktreeCreate !== 'function') {
return res.status(501).json({ error: 'Worktree validation is not available' });
}
try {
const directory = req.query.directory;
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory parameter is required' });
}
const result = await validateWorktreeCreate(directory, req.body || {});
res.json(result);
} catch (error) {
console.error('Failed to validate worktree creation:', error);
res.status(500).json({ error: error.message || 'Failed to validate worktree creation' });
}
});
app.post('/api/git/worktrees', async (req, res) => {
const { createWorktree } = await getGitLibraries();
if (typeof createWorktree !== 'function') {
return res.status(501).json({ error: 'Worktree creation is not available' });
}
try {
const directory = req.query.directory;
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory parameter is required' });
}
const created = await createWorktree(directory, req.body || {});
res.json(created);
} catch (error) {
console.error('Failed to create worktree:', error);
res.status(500).json({ error: error.message || 'Failed to create worktree' });
}
});
app.post('/api/git/worktrees/preview', async (req, res) => {
const { previewWorktreeCreate } = await getGitLibraries();
if (typeof previewWorktreeCreate !== 'function') {
return res.status(501).json({ error: 'Worktree preview is not available' });
}
try {
const directory = req.query.directory;
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory parameter is required' });
}
const preview = await previewWorktreeCreate(directory, req.body || {});
res.json(preview);
} catch (error) {
console.error('Failed to preview worktree:', error);
res.status(500).json({ error: error.message || 'Failed to preview worktree' });
}
});
app.get('/api/git/worktrees/bootstrap-status', async (req, res) => {
const { getWorktreeBootstrapStatus } = await getGitLibraries();
if (typeof getWorktreeBootstrapStatus !== 'function') {
return res.status(501).json({ error: 'Worktree bootstrap status is not available' });
}
try {
const directory = req.query.directory;
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory parameter is required' });
}
const status = await getWorktreeBootstrapStatus(directory);
res.json(status);
} catch (error) {
console.error('Failed to get worktree bootstrap status:', error);
res.status(500).json({ error: error.message || 'Failed to get worktree bootstrap status' });
}
});
app.delete('/api/git/worktrees', async (req, res) => {
const { removeWorktree } = await getGitLibraries();
if (typeof removeWorktree !== 'function') {
return res.status(501).json({ error: 'Worktree removal is not available' });
}
try {
const directory = req.query.directory;
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory parameter is required' });
}
const worktreeDirectory = typeof req.body?.directory === 'string' ? req.body.directory : '';
if (!worktreeDirectory) {
return res.status(400).json({ error: 'worktree directory is required' });
}
const result = await removeWorktree(directory, {
directory: worktreeDirectory,
deleteLocalBranch: req.body?.deleteLocalBranch === true,
});
res.json({ success: Boolean(result) });
} catch (error) {
console.error('Failed to remove worktree:', error);
res.status(500).json({ error: error.message || 'Failed to remove worktree' });
}
});
app.get('/api/git/worktree-type', async (req, res) => {
const { isLinkedWorktree } = await getGitLibraries();
try {
const { directory } = req.query;
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory parameter is required' });
}
const linked = await isLinkedWorktree(directory);
res.json({ linked });
} catch (error) {
console.error('Failed to determine worktree type:', error);
res.status(500).json({ error: error.message || 'Failed to determine worktree type' });
}
});
app.post('/api/git/validate-directory', async (req, res) => {
const { validateWorktreeDirectory } = await getGitLibraries();
if (typeof validateWorktreeDirectory !== 'function') {
return res.status(501).json({ error: 'validateWorktreeDirectory is not available' });
}
try {
const { directory, worktreeRoot } = req.body || {};
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory is required' });
}
if (!worktreeRoot || typeof worktreeRoot !== 'string') {
return res.status(400).json({ error: 'worktreeRoot is required' });
}
const result = await validateWorktreeDirectory(directory, worktreeRoot);
res.json(result);
} catch (error) {
console.error('Failed to validate worktree directory:', error);
res.status(500).json({ error: error.message || 'Failed to validate worktree directory' });
}
});
app.post('/api/git/canonicalize-worktree-state', async (req, res) => {
const { canonicalizeWorktreeState } = await getGitLibraries();
if (typeof canonicalizeWorktreeState !== 'function') {
return res.status(501).json({ error: 'canonicalizeWorktreeState is not available' });
}
try {
const { directory } = req.body || {};
if (!directory || typeof directory !== 'string') {
return res.status(400).json({ error: 'directory is required' });
}
const result = await canonicalizeWorktreeState(directory);
res.json(result);
} catch (error) {
console.error('Failed to canonicalize worktree state:', error);
res.status(500).json({ error: error.message || 'Failed to canonicalize worktree state' });
}
});
app.get('/api/git/log', async (req, res) => {
const { getLog } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const { maxCount, from, to, file } = req.query;
const log = await getLog(directory, {
maxCount: maxCount ? parseInt(maxCount) : undefined,
from,
to,
file
});
res.json(log);
} catch (error) {
console.error('Failed to get log:', error);
res.status(500).json({ error: error.message || 'Failed to get commit log' });
}
});
app.get('/api/git/commit-files', async (req, res) => {
const { getCommitFiles } = await getGitLibraries();
try {
const { directory, hash } = req.query;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
if (!hash) {
return res.status(400).json({ error: 'hash parameter is required' });
}
const result = await getCommitFiles(directory, hash);
res.json(result);
} catch (error) {
console.error('Failed to get commit files:', error);
res.status(500).json({ error: error.message || 'Failed to get commit files' });
}
});
}