UNPKG

@codewithmehmet/paul-cli

Version:

Intelligent project file scanner and Git change tracker with interactive interface

233 lines (227 loc) • 7.22 kB
import { execSync } from 'child_process'; import path from 'path'; import fs from 'fs'; const GIT_STATUS_MAP = { A: 'šŸ†• New file', M: 'šŸ“ Modified', D: 'šŸ—‘ļø Deleted', R: 'šŸ“‹ Renamed', '??': 'ā“ Untracked' }; export function getGitInfo() { try { const branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim(); return { branch }; } catch (error) { throw new Error('Not a git repository'); } } export function getGitStatus() { try { const output = execSync('git status --porcelain -u', { stdio: ['pipe', 'pipe', 'pipe'] }).toString(); const changes = []; for (const line of output.split('\n')) { if (!line) continue; const [staged, workspace] = [line[0], line[1]]; const file = line.slice(3); // Handle staged files if (staged !== ' ' && staged !== '?') { const type = workspace !== ' ' ? 'both' : 'staged'; changes.push({ file, status: staged, type }); } // Handle unstaged files if (workspace !== ' ' && staged === ' ') { changes.push({ file, status: workspace, type: 'unstaged' }); } else if (staged === '?' && workspace === '?') { // Untracked files changes.push({ file, status: '??', type: 'unstaged' }); } } return changes; } catch (error) { console.error('Error getting git status:', error); return []; } } export function getGitDiff(file, staged) { try { const gitRoot = findGitRoot(); const absolutePath = path.resolve(file); let relativePath = path.relative(gitRoot, absolutePath); relativePath = relativePath.replace(/\\/g, '/'); const command = staged ? `git diff --cached --unified=3 -- "${relativePath}"` : `git diff --unified=3 -- "${relativePath}"`; const diffOutput = execSync(command, { stdio: ['pipe', 'pipe', 'pipe'], cwd: gitRoot }).toString(); if (!diffOutput.trim()) { // Check if it's a new file if (!staged && fs.existsSync(file)) { const content = fs.readFileSync(file, 'utf8'); return content.split('\n').map(line => `+${line}`).join('\n'); } return '[No changes or new file]'; } return formatDiffOutput(diffOutput); } catch (error) { console.error('Error getting diff:', error); return '[No diff available]'; } } export function formatGitStatus(status) { return GIT_STATUS_MAP[status] || `ā“ Status ${status}`; } export function findGitRoot() { try { const gitRoot = execSync('git rev-parse --show-toplevel', { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() }).toString().trim(); return gitRoot; } catch (error) { throw new Error('Not a git repository'); } } export function getGitCommitChanges(commit) { try { // Verify commit exists execSync(`git rev-parse --verify ${commit}`, { stdio: 'pipe' }); // Check if it's a merge commit const parents = execSync(`git rev-list --parents -n 1 ${commit}`).toString().trim().split(' '); if (parents.length > 2) { console.log(`āš ļø Commit ${commit} is a merge commit with no direct changes`); return []; } // Get files changed in this commit const output = execSync(`git diff-tree --no-commit-id --name-status -r ${commit}`).toString(); const changes = []; if (!output.trim()) { return changes; } for (const line of output.split('\n')) { if (!line.trim()) continue; const parts = line.split('\t'); if (parts.length < 2) continue; const status = parts[0]; const file = parts[1]; // Handle renamed files if (status.startsWith('R')) { const newFile = parts[2] || file; changes.push({ file: newFile, status: 'R', type: 'staged' }); } else { changes.push({ file, status, type: 'staged' }); } } console.log(`Found ${changes.length} file(s) changed in commit ${commit}`); return changes; } catch (error) { console.error('Error getting commit changes:', error); throw new Error(`Commit "${commit}" not found or invalid`); } } export function getGitCommitDiff(file, commit) { try { const relativePath = file.replace(/\\/g, '/'); const command = `git diff-tree --no-commit-id -p ${commit} -- "${relativePath}"`; const diffOutput = execSync(command).toString(); if (!diffOutput.trim()) { // Check if file was added in this commit try { const statusCommand = `git diff-tree --no-commit-id --name-status -r ${commit}`; const statusOutput = execSync(statusCommand).toString(); const fileStatus = statusOutput.split('\n').find(line => line.includes(relativePath)); if (fileStatus && fileStatus.startsWith('A')) { // File was added, show all content as added const content = execSync(`git show ${commit}:"${relativePath}"`).toString(); return content.split('\n').map(line => `+${line}`).join('\n'); } } catch { // Ignore error } return '[No changes in this commit]'; } return formatDiffOutput(diffOutput); } catch (error) { return '[No diff available]'; } } export function getCommitsSince(sinceCommit) { try { // Verify commit exists execSync(`git rev-parse --verify ${sinceCommit}`, { stdio: 'pipe' }); // Get all commits from sinceCommit (excluded) to HEAD const output = execSync(`git rev-list ${sinceCommit}..HEAD --reverse`).toString().trim(); return output ? output.split('\n') : []; } catch (error) { console.error('Error getting commits since:', error); throw new Error(`Commit "${sinceCommit}" not found`); } } export function getCommitMessage(commit) { try { const message = execSync(`git log --format=%s -n 1 ${commit}`).toString().trim(); return message || 'No commit message'; } catch { return 'No commit message'; } } export function getAllChangesSince(sinceCommit) { try { // Get cumulative diff since a commit const filesOutput = execSync(`git diff --name-status ${sinceCommit}`).toString(); const changes = []; for (const line of filesOutput.split('\n')) { if (!line.trim()) continue; const parts = line.split('\t'); if (parts.length >= 2) { changes.push({ file: parts[1], status: parts[0], type: 'staged' }); } } return changes; } catch (error) { return []; } } function formatDiffOutput(diff) { return diff.split('\n').filter(line => line.startsWith('+') || line.startsWith('-') || line.startsWith('@') || !line.startsWith('diff ') && !line.startsWith('index ') && !line.startsWith('--- ') && !line.startsWith('+++ ')).map(line => { if (line.startsWith('@@ ')) { const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); return match ? `\nšŸ“ Line ${match[2]}:` : line; } return line; }).filter(line => line !== '').join('\n'); }