git-fresh
Version:
Quickly reset your Git working directory to a clean state without re-cloning. Stashes, wipes, restores, and pops.
325 lines ⢠12.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.gitFresh = gitFresh;
const chalk_1 = __importDefault(require("chalk"));
const child_process_1 = require("child_process");
const fs_1 = require("fs");
const glob_1 = require("glob");
const inquirer_1 = __importDefault(require("inquirer"));
const ora_1 = __importDefault(require("ora"));
const path_1 = require("path");
/**
* Checks if the current directory is a Git repository
*/
function isGitRepository() {
return (0, fs_1.existsSync)('.git');
}
/**
* Executes a git command and returns the output
*/
function executeGitCommand(command) {
try {
return (0, child_process_1.execSync)(`git ${command}`, {
encoding: 'utf-8',
stdio: 'pipe',
windowsHide: true, // Hide console window on Windows
maxBuffer: 1024 * 1024 // 1MB buffer for large outputs
});
}
catch (error) {
throw new Error(`Git command failed: ${command}`);
}
}
/**
* Checks if there are any changes to stash
*/
function hasChangesToStash() {
try {
const status = executeGitCommand('status --porcelain');
return status.trim().length > 0;
}
catch {
return false;
}
}
/**
* Finds environment files matching various patterns
*/
async function findEnvFiles() {
const patterns = [
'.env',
'.env.*',
'*.env',
'.*.env',
'**/.env',
'**/.env.*',
'**/.*env',
'**/*.env'
];
const envFiles = new Set();
for (const pattern of patterns) {
try {
const matches = await (0, glob_1.glob)(pattern, {
dot: true,
ignore: ['node_modules/**', '.git/**']
});
matches.forEach(file => envFiles.add(file));
}
catch (error) {
// Ignore glob errors for individual patterns
}
}
return Array.from(envFiles).sort();
}
/**
* Prompts user to select which env files to ignore
*/
async function selectEnvFilesToIgnore(envFiles) {
if (envFiles.length === 0) {
return [];
}
console.log(chalk_1.default.yellow('\nš Environment files detected:'));
envFiles.forEach((file) => {
console.log(chalk_1.default.gray(` ${file}`));
});
const { action } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do with these environment files?',
choices: [
{ name: 'Ignore all (recommended)', value: 'all' },
{ name: 'Select individually', value: 'select' },
{ name: 'Ignore none (remove all)', value: 'none' }
],
default: 'all'
}
]);
if (action === 'all') {
return envFiles;
}
else if (action === 'none') {
return [];
}
else {
const { selectedFiles } = await inquirer_1.default.prompt([
{
type: 'checkbox',
name: 'selectedFiles',
message: 'Select files to ignore (keep safe):',
choices: envFiles.map(file => ({ name: file, value: file, checked: true })),
}
]);
return selectedFiles;
}
}
/**
* Removes all files and directories except .git and ignored files
*/
function removeAllExceptGitAndIgnored(ignoredFiles = []) {
const items = (0, fs_1.readdirSync)('.');
// Normalize all ignored file paths to use consistent separators across platforms
const normalizedIgnoredFiles = ignoredFiles.map(file => {
// Use posix.normalize to ensure forward slashes for consistent comparison
return path_1.posix.normalize(file.replace(/\\/g, '/'));
});
const ignoredSet = new Set(normalizedIgnoredFiles);
// Function to normalize path to use forward slashes for consistent comparison
function normalizePath(path) {
// Convert to forward slashes and normalize
return path_1.posix.normalize(path.replace(/\\/g, '/'));
}
// Function to join paths in a cross-platform way but normalize for comparison
function joinAndNormalize(...paths) {
return normalizePath((0, path_1.join)(...paths));
}
// Function to check if a path should be preserved
function shouldPreservePath(itemPath) {
const normalizedItemPath = normalizePath(itemPath);
// Always preserve .git
if (normalizedItemPath === '.git') {
return true;
}
// Check exact match with ignored files
if (ignoredSet.has(normalizedItemPath)) {
return true;
}
// Check if any ignored file is inside this directory
for (const ignoredFile of normalizedIgnoredFiles) {
if (ignoredFile.startsWith(normalizedItemPath + '/')) {
return true;
}
}
return false;
}
// Function to recursively process directories
function processDirectory(dirPath) {
try {
const dirItems = (0, fs_1.readdirSync)(dirPath);
for (const item of dirItems) {
// Use join for proper path construction, then normalize for comparison
const itemPath = dirPath === '.' ? item : (0, path_1.join)(dirPath, item);
const normalizedItemPath = normalizePath(itemPath);
if (shouldPreservePath(itemPath)) {
continue; // Skip this item as it should be preserved
}
try {
const stats = (0, fs_1.statSync)(itemPath);
if (stats.isDirectory()) {
// Check if this directory contains any files we need to preserve
let hasPreservedContent = false;
for (const ignoredFile of normalizedIgnoredFiles) {
if (ignoredFile.startsWith(normalizedItemPath + '/')) {
hasPreservedContent = true;
break;
}
}
if (hasPreservedContent) {
// Process the directory contents recursively
processDirectory(itemPath);
}
else {
// Remove the entire directory
(0, fs_1.rmSync)(itemPath, { recursive: true, force: true });
}
}
else {
// Remove the file
(0, fs_1.rmSync)(itemPath, { force: true });
}
}
catch (error) {
console.warn(chalk_1.default.yellow(`Warning: Could not remove ${itemPath}`));
}
}
}
catch (error) {
console.warn(chalk_1.default.yellow(`Warning: Could not read directory ${dirPath}`));
}
}
// Start processing from the root directory
processDirectory('.');
}
/**
* Finds files matching the specified glob pattern
*/
async function findGlobFiles(pattern) {
try {
const matches = await (0, glob_1.glob)(pattern, {
dot: true,
ignore: ['node_modules/**', '.git/**']
});
return matches.sort();
}
catch (error) {
console.warn(chalk_1.default.yellow(`Warning: Could not process glob pattern "${pattern}"`));
return [];
}
}
/**
* Main function that performs the git fresh operation
*/
async function gitFresh(options = {}) {
console.log(chalk_1.default.blue.bold('š Git Fresh - Resetting working directory\n'));
// Check if we're in a git repository
if (!isGitRepository()) {
throw new Error('This is not a Git repository. Please run this command in a Git repository.');
}
// Platform-specific checks and adjustments
const platform = process.platform;
const isWindows = platform === 'win32';
const isUnix = platform === 'linux' || platform === 'darwin';
// Log platform info for debugging (only in verbose mode if needed)
// console.log(chalk.gray(`Platform: ${platform} | Windows: ${isWindows} | Unix: ${isUnix}`));
let filesToIgnore = [];
// Handle glob pattern files if provided
if (options.ignoreGlobFiles) {
const globFiles = await findGlobFiles(options.ignoreGlobFiles);
if (globFiles.length > 0) {
filesToIgnore.push(...globFiles);
console.log(chalk_1.default.green(`š Protecting ${globFiles.length} file(s) matching pattern "${options.ignoreGlobFiles}":`));
globFiles.forEach(file => {
console.log(chalk_1.default.gray(` ā ${file}`));
});
}
else {
console.log(chalk_1.default.gray(`ā¹ No files found matching pattern "${options.ignoreGlobFiles}"`));
}
}
// Handle environment files if requested
if (options.ignoreEnvFiles) {
const envFiles = await findEnvFiles();
if (envFiles.length > 0) {
if (options.skipConfirmation) {
filesToIgnore.push(...envFiles);
console.log(chalk_1.default.green(`š Protecting ${envFiles.length} environment file(s) from removal`));
envFiles.forEach(file => {
console.log(chalk_1.default.gray(` ā ${file}`));
});
}
else {
const selectedEnvFiles = await selectEnvFilesToIgnore(envFiles);
filesToIgnore.push(...selectedEnvFiles);
if (selectedEnvFiles.length > 0) {
console.log(chalk_1.default.green(`\nš Will protect ${selectedEnvFiles.length} environment file(s):`));
selectedEnvFiles.forEach(file => {
console.log(chalk_1.default.gray(` ā ${file}`));
});
}
}
}
else {
console.log(chalk_1.default.gray('ā¹ No environment files found'));
}
}
let stashCreated = false;
try {
// Step 1: Stash changes if any exist
if (hasChangesToStash()) {
const stashSpinner = (0, ora_1.default)('Stashing current changes...').start();
try {
executeGitCommand('stash push -u -m "git-fresh: temporary stash"');
stashSpinner.succeed(chalk_1.default.green('ā Changes stashed successfully'));
stashCreated = true;
}
catch (error) {
stashSpinner.fail(chalk_1.default.red('ā Failed to stash changes'));
throw new Error('Cannot proceed: Failed to stash changes. Your files are safe, but git-fresh cannot continue without successfully stashing changes.');
}
}
else {
console.log(chalk_1.default.gray('ā¹ No changes to stash'));
}
// Step 2: Remove all files except .git and ignored files
const removeSpinner = (0, ora_1.default)('Removing files except .git and protected files...').start();
removeAllExceptGitAndIgnored(filesToIgnore);
removeSpinner.succeed(chalk_1.default.green('ā Files removed successfully'));
// Step 3: Restore all files from git
const restoreSpinner = (0, ora_1.default)('Restoring files from Git...').start();
executeGitCommand('restore .');
restoreSpinner.succeed(chalk_1.default.green('ā Files restored successfully'));
// Step 4: Pop the stash if we created one
if (stashCreated) {
const popSpinner = (0, ora_1.default)('Applying stashed changes...').start();
try {
executeGitCommand('stash pop');
popSpinner.succeed(chalk_1.default.green('ā Stashed changes applied successfully'));
}
catch (error) {
popSpinner.warn(chalk_1.default.yellow('ā Could not apply stashed changes (conflicts may exist)'));
console.log(chalk_1.default.yellow('Run "git stash list" to see your stashed changes'));
}
}
console.log(chalk_1.default.green.bold('\nš Git working directory reset successfully!'));
if (filesToIgnore.length > 0) {
console.log(chalk_1.default.cyan(`\nš Protected files were preserved and remain untouched.`));
}
}
catch (error) {
throw new Error(`Failed to reset Git working directory: ${error instanceof Error ? error.message : String(error)}`);
}
}
//# sourceMappingURL=index.js.map