@codewithmehmet/paul-cli
Version:
Intelligent project file scanner and Git change tracker with interactive interface
233 lines (227 loc) ⢠7.22 kB
JavaScript
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');
}