* chore: add .worktrees/ to gitignore for worktree workflow * feat(git): add getCommitFileDiff service function * docs(git): document getCommitFileDiff in module docs * feat(git): add GET /api/git/commit-file-diff route * feat(git): add CommitFileDiffResponse type and GitAPI method signature * feat(git): add getCommitFileDiff HTTP client function * feat(git): add getCommitFileDiff API facade * feat(git): add getCommitFileDiff stub to VS Code bridge * feat(git): add getCommitFileDiff to VS Code gitService and bridge handler * feat(git): add inline file diff to history commit rows * fix(git): consolidate CommitFileDiffResponse import to gitApi facade * fix(git): pass directory through history, validate hash, propagate git errors * fix(git): use exit code check for VS Code getCommitFileDiff error detection * fix(git): VS Code rename detection, hash validation parity, retry on error * fix(git): register scroll container as virtualizer root to fix empty space in history diffs * fix(git): address greptile review — rename key extraction, directory guard, language detection, isBinary cleanup * fix(git): harden history inline diffs --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
973 lines
34 KiB
JavaScript
973 lines
34 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' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/git/commit-file-diff', async (req, res) => {
|
|
const { getCommitFileDiff } = await getGitLibraries();
|
|
try {
|
|
const { directory, hash, path: filePath } = req.query;
|
|
if (!directory || typeof directory !== 'string') {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
if (!hash || typeof hash !== 'string') {
|
|
return res.status(400).json({ error: 'hash parameter is required' });
|
|
}
|
|
if (!/^[0-9a-fA-F]{7,40}$/.test(hash)) {
|
|
return res.status(400).json({ error: 'hash must be a valid commit SHA' });
|
|
}
|
|
if (!filePath || typeof filePath !== 'string') {
|
|
return res.status(400).json({ error: 'path parameter is required' });
|
|
}
|
|
|
|
const isBinary = req.query.binary === 'true';
|
|
const result = await getCommitFileDiff(directory, hash, filePath, isBinary);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to get commit file diff:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to get commit file diff' });
|
|
}
|
|
});
|
|
|
|
}
|