git-tweezers
Version:
Advanced git staging tool with hunk and line-level control
145 lines (144 loc) • 6.59 kB
JavaScript
import { Command, Flags, Args } from '@oclif/core';
import chalk from 'chalk';
import { StagingService } from '../services/staging-service.js';
import { logger, LogLevel } from '../utils/logger.js';
import { StagingError } from '../utils/staging-error.js';
import { parseFileSelector } from '../utils/file-parser.js';
import { GitWrapper } from '../core/git-wrapper.js';
export default class Hunk extends Command {
static description = 'Stage specific hunks from a file by their numbers';
static examples = [
'<%= config.bin %> <%= command.id %> src/index.ts 2',
'<%= config.bin %> <%= command.id %> src/index.ts:2 # Alternative syntax',
'<%= config.bin %> <%= command.id %> -p src/index.ts 1 # Use precise mode',
'<%= config.bin %> <%= command.id %> src/index.ts 1,3,5 # Stage multiple hunks',
'<%= config.bin %> <%= command.id %> src/file1.ts:1 src/file2.ts:3 # Multiple files',
];
static flags = {
precise: Flags.boolean({
char: 'p',
description: 'Use precise mode (U0 context) for finer control',
default: false,
}),
'dry-run': Flags.boolean({
char: 'd',
description: 'Show what would be staged without applying changes',
default: false,
}),
};
static args = {
selectors: Args.string({
description: 'File and hunk selectors (e.g., file.ts:1 or file.ts 1)',
required: true,
}),
};
static strict = false; // Allow multiple arguments
async run() {
const { argv, flags } = await this.parse(Hunk);
const precise = flags.precise;
const dryRun = flags['dry-run'];
if (process.env.DEBUG === '1') {
logger.setLevel(LogLevel.DEBUG);
}
try {
const git = new GitWrapper();
const staging = new StagingService(git.gitRoot);
// Parse arguments to extract file and hunk selectors
const fileHunks = new Map();
// Handle both syntaxes:
// 1. file.ts:1,2,3 or file.ts:abc
// 2. file.ts 1,2,3 or file.ts 1 2 3
let i = 0;
while (i < argv.length) {
const arg = argv[i];
const parsed = parseFileSelector(arg);
if (parsed.selector) {
// file:selector syntax
const selectors = parsed.selector.split(',').map(s => s.trim());
const existing = fileHunks.get(parsed.file) || [];
fileHunks.set(parsed.file, [...existing, ...selectors]);
i++;
}
else {
// This is a file without selector, collect selectors from following args
const selectors = [];
let j = i + 1;
// Collect all following arguments that look like selectors (not file paths)
while (j < argv.length) {
const potentialSelector = argv[j];
// If it looks like a file path (has extension or path separator), stop collecting
if (potentialSelector.includes('.') || potentialSelector.includes('/') || potentialSelector.includes('\\')) {
break;
}
// If it has a colon, it's a file:selector syntax, stop collecting
if (potentialSelector.includes(':')) {
break;
}
// Otherwise, it's a selector
selectors.push(...potentialSelector.split(',').map(s => s.trim()));
j++;
}
if (selectors.length === 0) {
throw new Error(`Invalid syntax: expected hunk selector after ${arg}`);
}
const existing = fileHunks.get(parsed.file) || [];
fileHunks.set(parsed.file, [...existing, ...selectors]);
i = j; // Skip all processed selectors
}
}
if (fileHunks.size === 0) {
throw new Error('No files or hunks specified');
}
if (precise) {
logger.info('Using precise mode (U0 context)');
}
// Track staging summary
let totalHunks = 0;
const stagedFiles = [];
const stagedDetails = [];
// Stage hunks for each file
for (const [file, selectors] of fileHunks) {
if (selectors.length === 1) {
await staging.stageHunk(file, selectors[0], { precise, dryRun });
if (!dryRun) {
logger.success(`Staged hunk ${selectors[0]} from ${file}`);
totalHunks += 1;
stagedFiles.push(file);
stagedDetails.push(`${file}:${selectors[0]}`);
}
}
else {
await staging.stageHunks(file, selectors, { precise, dryRun });
if (!dryRun) {
logger.success(`Staged hunks ${selectors.join(', ')} from ${file}`);
totalHunks += selectors.length;
stagedFiles.push(file);
stagedDetails.push(`${file}:${selectors.join(',')}`);
}
}
}
// Show summary
if (!dryRun && totalHunks > 0) {
this.log('');
this.log(chalk.green('─'.repeat(60)));
this.log(chalk.green.bold(`✓ Successfully staged ${totalHunks} hunk${totalHunks > 1 ? 's' : ''} across ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`));
if (stagedDetails.length <= 3) {
stagedDetails.forEach(detail => {
this.log(chalk.green(` • ${detail}`));
});
}
this.log(chalk.green('─'.repeat(60)));
}
}
catch (error) {
if (error instanceof StagingError) {
// Display formatted error with remaining hunks info
this.log(error.getFormattedMessage());
}
else {
logger.error(error instanceof Error ? error.message : String(error));
}
this.exit(1);
}
}
}