mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-10 04:59:38 +00:00
Update package dependencies, add Git API routes, and implement audio transcription functionality. Introduce new components for Git management, enhance chat interface with microphone support, and improve UI elements for better user experience.
This commit is contained in:
388
server/routes/git.js
Executable file
388
server/routes/git.js
Executable file
@@ -0,0 +1,388 @@
|
||||
const express = require('express');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const router = express.Router();
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Helper function to get the actual project path from the encoded project name
|
||||
function getActualProjectPath(projectName) {
|
||||
// Claude stores projects with dashes instead of slashes
|
||||
// Convert "-Users-dmieloch-Dev-experiments-claudecodeui" to "/Users/dmieloch/Dev/experiments/claudecodeui"
|
||||
return projectName.replace(/-/g, '/');
|
||||
}
|
||||
|
||||
// Get git status for a project
|
||||
router.get('/status', async (req, res) => {
|
||||
const { project } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
console.log('Git status for project:', project, '-> path:', projectPath);
|
||||
|
||||
// Check if directory exists
|
||||
try {
|
||||
await fs.access(projectPath);
|
||||
} catch {
|
||||
console.error('Project path not found:', projectPath);
|
||||
return res.json({ error: 'Project not found' });
|
||||
}
|
||||
|
||||
// Check if it's a git repository
|
||||
try {
|
||||
await execAsync('git rev-parse --git-dir', { cwd: projectPath });
|
||||
} catch {
|
||||
console.error('Not a git repository:', projectPath);
|
||||
return res.json({ error: 'Not a git repository' });
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
|
||||
|
||||
// Get git status
|
||||
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectPath });
|
||||
|
||||
const modified = [];
|
||||
const added = [];
|
||||
const deleted = [];
|
||||
const untracked = [];
|
||||
|
||||
statusOutput.split('\n').forEach(line => {
|
||||
if (!line.trim()) return;
|
||||
|
||||
const status = line.substring(0, 2);
|
||||
const file = line.substring(3);
|
||||
|
||||
if (status === 'M ' || status === ' M' || status === 'MM') {
|
||||
modified.push(file);
|
||||
} else if (status === 'A ' || status === 'AM') {
|
||||
added.push(file);
|
||||
} else if (status === 'D ' || status === ' D') {
|
||||
deleted.push(file);
|
||||
} else if (status === '??') {
|
||||
untracked.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
branch: branch.trim(),
|
||||
modified,
|
||||
added,
|
||||
deleted,
|
||||
untracked
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Git status error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get diff for a specific file
|
||||
router.get('/diff', async (req, res) => {
|
||||
const { project, file } = req.query;
|
||||
|
||||
if (!project || !file) {
|
||||
return res.status(400).json({ error: 'Project name and file path are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Check if file is untracked
|
||||
const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
|
||||
const isUntracked = statusOutput.startsWith('??');
|
||||
|
||||
let diff;
|
||||
if (isUntracked) {
|
||||
// For untracked files, show the entire file content as additions
|
||||
const fileContent = await fs.readFile(path.join(projectPath, file), 'utf-8');
|
||||
const lines = fileContent.split('\n');
|
||||
diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
|
||||
lines.map(line => `+${line}`).join('\n');
|
||||
} else {
|
||||
// Get diff for tracked files
|
||||
const { stdout } = await execAsync(`git diff HEAD -- "${file}"`, { cwd: projectPath });
|
||||
diff = stdout || '';
|
||||
|
||||
// If no unstaged changes, check for staged changes
|
||||
if (!diff) {
|
||||
const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath });
|
||||
diff = stagedDiff;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ diff });
|
||||
} catch (error) {
|
||||
console.error('Git diff error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Commit changes
|
||||
router.post('/commit', async (req, res) => {
|
||||
const { project, message, files } = req.body;
|
||||
|
||||
if (!project || !message || !files || files.length === 0) {
|
||||
return res.status(400).json({ error: 'Project name, commit message, and files are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Stage selected files
|
||||
for (const file of files) {
|
||||
await execAsync(`git add "${file}"`, { cwd: projectPath });
|
||||
}
|
||||
|
||||
// Commit with message
|
||||
const { stdout } = await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: projectPath });
|
||||
|
||||
res.json({ success: true, output: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git commit error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get list of branches
|
||||
router.get('/branches', async (req, res) => {
|
||||
const { project } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
console.log('Git branches for project:', project, '-> path:', projectPath);
|
||||
|
||||
// Get all branches
|
||||
const { stdout } = await execAsync('git branch -a', { cwd: projectPath });
|
||||
|
||||
// Parse branches
|
||||
const branches = stdout
|
||||
.split('\n')
|
||||
.map(branch => branch.trim())
|
||||
.filter(branch => branch && !branch.includes('->')) // Remove empty lines and HEAD pointer
|
||||
.map(branch => {
|
||||
// Remove asterisk from current branch
|
||||
if (branch.startsWith('* ')) {
|
||||
return branch.substring(2);
|
||||
}
|
||||
// Remove remotes/ prefix
|
||||
if (branch.startsWith('remotes/origin/')) {
|
||||
return branch.substring(15);
|
||||
}
|
||||
return branch;
|
||||
})
|
||||
.filter((branch, index, self) => self.indexOf(branch) === index); // Remove duplicates
|
||||
|
||||
res.json({ branches });
|
||||
} catch (error) {
|
||||
console.error('Git branches error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Checkout branch
|
||||
router.post('/checkout', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Checkout the branch
|
||||
const { stdout } = await execAsync(`git checkout "${branch}"`, { cwd: projectPath });
|
||||
|
||||
res.json({ success: true, output: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git checkout error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new branch
|
||||
router.post('/create-branch', async (req, res) => {
|
||||
const { project, branch } = req.body;
|
||||
|
||||
if (!project || !branch) {
|
||||
return res.status(400).json({ error: 'Project name and branch name are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Create and checkout new branch
|
||||
const { stdout } = await execAsync(`git checkout -b "${branch}"`, { cwd: projectPath });
|
||||
|
||||
res.json({ success: true, output: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git create branch error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get recent commits
|
||||
router.get('/commits', async (req, res) => {
|
||||
const { project, limit = 10 } = req.query;
|
||||
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Project name is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Get commit log with stats
|
||||
const { stdout } = await execAsync(
|
||||
`git log --pretty=format:'%H|%an|%ae|%ad|%s' --date=relative -n ${limit}`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
|
||||
const commits = stdout
|
||||
.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => {
|
||||
const [hash, author, email, date, ...messageParts] = line.split('|');
|
||||
return {
|
||||
hash,
|
||||
author,
|
||||
email,
|
||||
date,
|
||||
message: messageParts.join('|')
|
||||
};
|
||||
});
|
||||
|
||||
// Get stats for each commit
|
||||
for (const commit of commits) {
|
||||
try {
|
||||
const { stdout: stats } = await execAsync(
|
||||
`git show --stat --format='' ${commit.hash}`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
commit.stats = stats.trim().split('\n').pop(); // Get the summary line
|
||||
} catch (error) {
|
||||
commit.stats = '';
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ commits });
|
||||
} catch (error) {
|
||||
console.error('Git commits error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get diff for a specific commit
|
||||
router.get('/commit-diff', async (req, res) => {
|
||||
const { project, commit } = req.query;
|
||||
|
||||
if (!project || !commit) {
|
||||
return res.status(400).json({ error: 'Project name and commit hash are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Get diff for the commit
|
||||
const { stdout } = await execAsync(
|
||||
`git show ${commit}`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
|
||||
res.json({ diff: stdout });
|
||||
} catch (error) {
|
||||
console.error('Git commit diff error:', error);
|
||||
res.json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Generate commit message based on staged changes
|
||||
router.post('/generate-commit-message', async (req, res) => {
|
||||
const { project, files } = req.body;
|
||||
|
||||
if (!project || !files || files.length === 0) {
|
||||
return res.status(400).json({ error: 'Project name and files are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const projectPath = getActualProjectPath(project);
|
||||
|
||||
// Get diff for selected files
|
||||
let combinedDiff = '';
|
||||
for (const file of files) {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git diff HEAD -- "${file}"`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
if (stdout) {
|
||||
combinedDiff += `\n--- ${file} ---\n${stdout}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting diff for ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Use AI to generate commit message (simple implementation)
|
||||
// In a real implementation, you might want to use GPT or Claude API
|
||||
const message = generateSimpleCommitMessage(files, combinedDiff);
|
||||
|
||||
res.json({ message });
|
||||
} catch (error) {
|
||||
console.error('Generate commit message error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Simple commit message generator (can be replaced with AI)
|
||||
function generateSimpleCommitMessage(files, diff) {
|
||||
const fileCount = files.length;
|
||||
const isMultipleFiles = fileCount > 1;
|
||||
|
||||
// Analyze the diff to determine the type of change
|
||||
const additions = (diff.match(/^\+[^+]/gm) || []).length;
|
||||
const deletions = (diff.match(/^-[^-]/gm) || []).length;
|
||||
|
||||
// Determine the primary action
|
||||
let action = 'Update';
|
||||
if (additions > 0 && deletions === 0) {
|
||||
action = 'Add';
|
||||
} else if (deletions > 0 && additions === 0) {
|
||||
action = 'Remove';
|
||||
} else if (additions > deletions * 2) {
|
||||
action = 'Enhance';
|
||||
} else if (deletions > additions * 2) {
|
||||
action = 'Refactor';
|
||||
}
|
||||
|
||||
// Generate message based on files
|
||||
if (isMultipleFiles) {
|
||||
const components = new Set(files.map(f => {
|
||||
const parts = f.split('/');
|
||||
return parts[parts.length - 2] || parts[0];
|
||||
}));
|
||||
|
||||
if (components.size === 1) {
|
||||
return `${action} ${[...components][0]} component`;
|
||||
} else {
|
||||
return `${action} multiple components`;
|
||||
}
|
||||
} else {
|
||||
const fileName = files[0].split('/').pop();
|
||||
const componentName = fileName.replace(/\.(jsx?|tsx?|css|scss)$/, '');
|
||||
return `${action} ${componentName}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user