Show every change on the current branch relative to its base in the Changed/Staged/Last turn dropdown. The base comes from the branch's reflog record or an explicit per-branch user choice (persisted), never a main/master guess; when git has no record the user picks a base once from a searchable branch list. - server: GET /api/git/branch-base (reflog-derived base), GET /api/git/range-files (name-status -z with rename/copy destination paths and -C copy detection) - shared UI: optional getBranchBase/getGitRangeFiles runtime APIs with boundary parsing; persisted per-branch overrides keyed by runtime+directory+branch - DiffView: branch scope with confirmed-unavailability coercion of persisted tabs (detached HEAD, default-branch checkout, metadata settled without a default), range-invalidated diff cache guarded against stale completions, bounded branch-metadata retry, read-only diff actions in branch scope; hidden in VS Code - helper module branchDiffScope.ts with tests for coercion, availability, race conditions, and retry exhaustion
1314 lines
46 KiB
JavaScript
1314 lines
46 KiB
JavaScript
export function registerGitRoutes(app) {
|
|
let gitLibraries = null;
|
|
const getGitLibraries = async () => {
|
|
if (!gitLibraries) {
|
|
gitLibraries = await import('./index.js');
|
|
}
|
|
return gitLibraries;
|
|
};
|
|
|
|
const resolveDirectoryQuery = (value) => {
|
|
const raw = Array.isArray(value) ? value[0] : value;
|
|
if (typeof raw !== 'string') {
|
|
return null;
|
|
}
|
|
const trimmed = raw.trim();
|
|
return trimmed || null;
|
|
};
|
|
|
|
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 : '';
|
|
const fallback = !message && error != null ? String(error) : '';
|
|
return [message, stderr, stdout, fallback]
|
|
.map((value) => String(value || '').trim())
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
};
|
|
|
|
const isNonRepoGitError = (error) => /not a git repository/i.test(extractGitErrorText(error));
|
|
|
|
const nonRepoStatusPayload = () => ({
|
|
isGitRepository: false,
|
|
files: [],
|
|
branch: null,
|
|
ahead: 0,
|
|
behind: 0,
|
|
});
|
|
|
|
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 = resolveDirectoryQuery(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) {
|
|
if (isNonRepoGitError(error)) {
|
|
console.warn('Git check treated non-repository path as not a git repo:', extractGitErrorText(error));
|
|
return res.json({ isGitRepository: false });
|
|
}
|
|
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();
|
|
|
|
try {
|
|
const directory = resolveDirectoryQuery(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(nonRepoStatusPayload());
|
|
}
|
|
|
|
const mode = req.query.mode === 'light' ? 'light' : undefined;
|
|
const status = await getStatus(directory, { mode });
|
|
res.json(status);
|
|
} catch (error) {
|
|
// Non-repo / GitError must not abort callers that enumerate projects or
|
|
// sessions (e.g. sidebar discovery). Log a warning and continue.
|
|
if (isNonRepoGitError(error)) {
|
|
console.warn('Git status skipped for non-repository path:', extractGitErrorText(error));
|
|
return res.json(nonRepoStatusPayload());
|
|
}
|
|
console.error('Failed to get git status:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to get git status' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/git/primary-root', async (req, res) => {
|
|
const { resolvePrimaryWorktreeRoot } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
const result = await resolvePrimaryWorktreeRoot(directory);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to resolve git primary root:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to resolve git primary root' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/git/toplevel', async (req, res) => {
|
|
const { resolveWorktreeTopLevel } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
const result = await resolveWorktreeTopLevel(directory);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to resolve git worktree toplevel:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to resolve git worktree toplevel' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/git/commit-summaries', async (req, res) => {
|
|
const { getCommitSummaries } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
const result = await getCommitSummaries(directory, req.body?.shas);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to get git commit summaries:', error);
|
|
res.status(400).json({ error: error.message || 'Failed to get git commit summaries' });
|
|
}
|
|
});
|
|
|
|
const handleIntegrateAction = (action, loadHandler) => {
|
|
app.post(`/api/git/integrate/${action}`, async (req, res) => {
|
|
try {
|
|
const handler = await loadHandler();
|
|
const result = await handler(req.body || {});
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error(`Failed to run git integrate ${action}:`, error);
|
|
res.status(400).json({ error: error.message || `Failed to run git integrate ${action}` });
|
|
}
|
|
});
|
|
};
|
|
|
|
handleIntegrateAction('plan', async () => {
|
|
const { computeIntegratePlan } = await getGitLibraries();
|
|
return (body) => computeIntegratePlan(body);
|
|
});
|
|
|
|
handleIntegrateAction('conflict-details', async () => {
|
|
const { getIntegrateConflictDetails } = await getGitLibraries();
|
|
return (body) => getIntegrateConflictDetails(body?.tempWorktreePath);
|
|
});
|
|
|
|
handleIntegrateAction('cherry-pick-status', async () => {
|
|
const { isCherryPickInProgress } = await getGitLibraries();
|
|
return (body) => isCherryPickInProgress(body?.tempWorktreePath);
|
|
});
|
|
|
|
handleIntegrateAction('run', async () => {
|
|
const { integrateWorktreeCommits } = await getGitLibraries();
|
|
return (body) => integrateWorktreeCommits(body?.plan);
|
|
});
|
|
|
|
handleIntegrateAction('abort', async () => {
|
|
const { abortIntegrate } = await getGitLibraries();
|
|
return (body) => abortIntegrate(body?.state);
|
|
});
|
|
|
|
handleIntegrateAction('continue', async () => {
|
|
const { continueIntegrate } = await getGitLibraries();
|
|
return (body) => continueIntegrate(body?.state);
|
|
});
|
|
|
|
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.get('/api/git/range-diff', async (req, res) => {
|
|
const { getRangeDiff } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory || typeof directory !== 'string') {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
|
|
const base = req.query.base;
|
|
const head = req.query.head;
|
|
if (!base || typeof base !== 'string' || !head || typeof head !== 'string') {
|
|
return res.status(400).json({ error: 'base and head parameters are required' });
|
|
}
|
|
|
|
const pathParam = typeof req.query.path === 'string' && req.query.path ? req.query.path : undefined;
|
|
const context = req.query.context ? parseInt(String(req.query.context), 10) : undefined;
|
|
|
|
const diff = await getRangeDiff(directory, {
|
|
base,
|
|
head,
|
|
path: pathParam,
|
|
contextLines: Number.isFinite(context) ? context : 3,
|
|
});
|
|
|
|
res.json({ diff });
|
|
} catch (error) {
|
|
console.error('Failed to get git range diff:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to get git range diff' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/git/branch-base', async (req, res) => {
|
|
const { getBranchBase } = await getGitLibraries();
|
|
try {
|
|
const directory = resolveDirectoryQuery(req.query.directory);
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
|
|
const branch = resolveDirectoryQuery(req.query.branch);
|
|
if (!branch) {
|
|
return res.status(400).json({ error: 'branch parameter is required' });
|
|
}
|
|
|
|
const result = await getBranchBase(directory, branch);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to get branch base:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to get branch base' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/git/range-files', async (req, res) => {
|
|
const { getRangeFiles } = await getGitLibraries();
|
|
try {
|
|
const directory = resolveDirectoryQuery(req.query.directory);
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
|
|
const base = resolveDirectoryQuery(req.query.base);
|
|
const head = resolveDirectoryQuery(req.query.head);
|
|
if (!base || !head) {
|
|
return res.status(400).json({ error: 'base and head parameters are required' });
|
|
}
|
|
|
|
const files = await getRangeFiles(directory, { base, head });
|
|
res.json({ files });
|
|
} catch (error) {
|
|
console.error('Failed to get git range files:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to get git range files' });
|
|
}
|
|
});
|
|
|
|
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, scope } = req.body || {};
|
|
if (!path || typeof path !== 'string') {
|
|
return res.status(400).json({ error: 'path parameter is required' });
|
|
}
|
|
|
|
await revertFile(directory, path, { scope });
|
|
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/stage', async (req, res) => {
|
|
const { stageFiles } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
|
|
const { path, paths } = req.body || {};
|
|
const filePaths = Array.isArray(paths) ? paths : [path];
|
|
if (!filePaths.some((value) => typeof value === 'string' && value.trim())) {
|
|
return res.status(400).json({ error: 'path parameter is required' });
|
|
}
|
|
|
|
await stageFiles(directory, filePaths);
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to stage git file:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to stage git file' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/git/unstage', async (req, res) => {
|
|
const { unstageFiles } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
|
|
const { path, paths } = req.body || {};
|
|
const filePaths = Array.isArray(paths) ? paths : [path];
|
|
if (!filePaths.some((value) => typeof value === 'string' && value.trim())) {
|
|
return res.status(400).json({ error: 'path parameter is required' });
|
|
}
|
|
|
|
await unstageFiles(directory, filePaths);
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to unstage git file:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to unstage git file' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/git/apply-hunk', async (req, res) => {
|
|
const { applyHunk } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
|
|
const { path: filePath, patch, action } = req.body || {};
|
|
if (!filePath || typeof filePath !== 'string') {
|
|
return res.status(400).json({ error: 'path parameter is required' });
|
|
}
|
|
if (typeof patch !== 'string' || !patch.trim()) {
|
|
return res.status(400).json({ error: 'patch is required' });
|
|
}
|
|
if (action !== 'stage' && action !== 'unstage' && action !== 'discard') {
|
|
return res.status(400).json({ error: 'action must be stage, unstage, or discard' });
|
|
}
|
|
|
|
await applyHunk(directory, filePath, { patch, action });
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Failed to apply git hunk:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to apply git hunk' });
|
|
}
|
|
});
|
|
|
|
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, stageFiles } = req.body;
|
|
if (!message) {
|
|
return res.status(400).json({ error: 'message is required' });
|
|
}
|
|
|
|
const result = await commit(directory, message, {
|
|
addAll,
|
|
files,
|
|
stageFiles,
|
|
});
|
|
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.post('/api/git/checkout-commit', async (req, res) => {
|
|
const { checkoutCommit } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
const { hash } = req.body;
|
|
if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
|
|
return res.status(400).json({ error: 'Invalid commit hash' });
|
|
}
|
|
const result = await checkoutCommit(directory, hash);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to checkout commit:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to checkout commit' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/git/cherry-pick', async (req, res) => {
|
|
const { cherryPick } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
const { hash } = req.body;
|
|
if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
|
|
return res.status(400).json({ error: 'Invalid commit hash' });
|
|
}
|
|
const result = await cherryPick(directory, hash);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to cherry-pick:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to cherry-pick' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/git/revert-commit', async (req, res) => {
|
|
const { revertCommit } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
const { hash } = req.body;
|
|
if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
|
|
return res.status(400).json({ error: 'Invalid commit hash' });
|
|
}
|
|
const result = await revertCommit(directory, hash);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to revert commit:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to revert commit' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/git/reset-to-commit', async (req, res) => {
|
|
const { resetToCommit } = await getGitLibraries();
|
|
try {
|
|
const directory = req.query.directory;
|
|
if (!directory) {
|
|
return res.status(400).json({ error: 'directory parameter is required' });
|
|
}
|
|
const { hash, mode, force } = req.body;
|
|
if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
|
|
return res.status(400).json({ error: 'Invalid commit hash' });
|
|
}
|
|
if (!['soft', 'mixed', 'hard'].includes(mode)) {
|
|
return res.status(400).json({ error: 'mode must be soft, mixed, or hard' });
|
|
}
|
|
const result = await resetToCommit(directory, hash, mode, force === true);
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Failed to reset to commit:', error);
|
|
res.status(500).json({ error: error.message || 'Failed to reset' });
|
|
}
|
|
});
|
|
|
|
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 all = req.query.all === 'true';
|
|
const log = await getLog(directory, {
|
|
maxCount: maxCount ? parseInt(maxCount) : undefined,
|
|
from,
|
|
to,
|
|
file,
|
|
all
|
|
});
|
|
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' });
|
|
}
|
|
});
|
|
|
|
}
|