Files
coze-studio/common/autoinstallers/rush-commands/src/revert-useless-changes/utils/git.ts
tecvan-fe 3e498d032a feat(rush-commands): add revert-useless-changes tool with functional programming design
Implement a comprehensive tool to automatically detect and revert files with only
cosmetic changes (whitespace or comments) in Git repositories.

Features:
- Functional programming architecture with pure functions
- Rule-based analysis system (whitespace and AST comment detection)
- Commander.js CLI with comprehensive options
- ESM module system throughout
- Proper handling of new/added files
- TypeScript with strict typing
- Comprehensive documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 14:25:17 +08:00

218 lines
6.1 KiB
TypeScript

import { execSync } from 'child_process';
import { resolve, join, dirname } from 'path';
import { existsSync } from 'fs';
import { GitOptions } from '../types';
/**
* Get the project root directory (git repository root)
*/
export const getProjectRoot = (): string => {
try {
return execSync('git rev-parse --show-toplevel', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
} catch (error) {
throw new Error('Not in a git repository or git is not available');
}
};
/**
* Get list of changed files in git working directory (includes both modified and added files)
*/
export const getChangedFiles = (options: GitOptions = { cwd: process.cwd() }): string[] => {
try {
// Get both modified files and added files
const modifiedOutput = execSync('git diff --name-only', {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
});
const addedOutput = execSync('git diff --cached --name-only', {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
});
const modifiedFiles = modifiedOutput
.split('\n')
.map(file => file.trim())
.filter(file => file.length > 0);
const addedFiles = addedOutput
.split('\n')
.map(file => file.trim())
.filter(file => file.length > 0);
// Combine and deduplicate
const allFiles = Array.from(new Set([...modifiedFiles, ...addedFiles]));
return allFiles.map(file => resolve(options.cwd, file));
} catch (error) {
throw new Error(`Failed to get changed files: ${error instanceof Error ? error.message : String(error)}`);
}
};
/**
* Get list of modified files relative to a git reference
*/
export const getModifiedFiles = (options: GitOptions = { cwd: process.cwd() }): string[] => {
try {
const output = execSync(`git diff --name-only ${options.ref || 'HEAD'}`, {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
});
return output
.split('\n')
.map(file => file.trim())
.filter(file => file.length > 0);
} catch (error) {
throw new Error(`Failed to get modified files: ${error instanceof Error ? error.message : String(error)}`);
}
};
/**
* Get file content from a specific git reference
*/
export const getFileContentAtRef = (filePath: string, options: GitOptions): string => {
try {
const projectRoot = getProjectRoot();
const relativePath = filePath.startsWith(projectRoot)
? filePath.substring(projectRoot.length + 1)
: filePath;
return execSync(`git show ${options.ref || 'HEAD'}:${relativePath}`, {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
});
} catch (error) {
throw new Error(`Failed to get file content at ref: ${error instanceof Error ? error.message : String(error)}`);
}
};
/**
* Check if a file is newly added (not in HEAD)
*/
export const isNewFile = (filePath: string, options: GitOptions): boolean => {
try {
const projectRoot = getProjectRoot();
const relativePath = filePath.startsWith(projectRoot)
? filePath.substring(projectRoot.length + 1)
: filePath;
execSync(`git show ${options.ref || 'HEAD'}:${relativePath}`, {
cwd: options.cwd,
stdio: ['pipe', 'pipe', 'ignore']
});
// If git show succeeds, the file exists in HEAD, so it's not new
return false;
} catch (error) {
// If git show fails, the file doesn't exist in HEAD, so it's new
return true;
}
};
/**
* Check if a file has only whitespace changes
*/
export const hasOnlyWhitespaceChanges = (filePath: string, options: GitOptions): boolean => {
try {
// Skip analysis for new files
if (isNewFile(filePath, options)) {
return false;
}
const projectRoot = getProjectRoot();
const relativePath = filePath.startsWith(projectRoot)
? filePath.substring(projectRoot.length + 1)
: filePath;
const output = execSync(`git diff -w ${options.ref || 'HEAD'} -- "${relativePath}"`, {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
});
return output.trim() === '';
} catch (error) {
// If git diff fails, assume the file has changes
return false;
}
};
/**
* Revert a file to its state in the git reference
*/
export const revertFile = (filePath: string, options: GitOptions): void => {
try {
const projectRoot = getProjectRoot();
const relativePath = filePath.startsWith(projectRoot)
? filePath.substring(projectRoot.length + 1)
: filePath;
execSync(`git checkout ${options.ref || 'HEAD'} -- "${relativePath}"`, {
cwd: options.cwd,
stdio: ['pipe', 'pipe', 'pipe']
});
} catch (error) {
throw new Error(`Failed to revert file: ${error instanceof Error ? error.message : String(error)}`);
}
};
/**
* Check if we're in a git repository
*/
export const isGitRepository = (cwd: string = process.cwd()): boolean => {
try {
execSync('git rev-parse --git-dir', {
cwd,
stdio: ['pipe', 'pipe', 'ignore']
});
return true;
} catch {
return false;
}
};
/**
* Find git repository root by recursively searching for .git directory
* @param startDir Directory to start searching from
* @returns Git repository root path or null if not found
*/
export const findGitRepositoryRoot = (startDir: string): string | null => {
// Check if .git exists in current directory
const gitDir = join(startDir, '.git');
if (existsSync(gitDir)) {
return startDir;
}
// Recursively check parent directories
let currentDir = startDir;
while (currentDir !== dirname(currentDir)) {
const parentGitDir = join(currentDir, '.git');
if (existsSync(parentGitDir)) {
return currentDir;
}
currentDir = dirname(currentDir);
}
return null;
};
/**
* Validate that a directory is within a git repository
* @param cwd Directory to validate
* @throws Error if not in a git repository
*/
export const validateGitRepository = (cwd: string): void => {
const gitRoot = findGitRepositoryRoot(cwd);
if (!gitRoot) {
throw new Error(`Not a git repository (or any parent directory): ${cwd}`);
}
};