git-tweezers
Version:
Advanced git staging tool with hunk and line-level control
133 lines (132 loc) • 5.77 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 { DiffRenderer } from '../utils/diff-renderer.js';
import { GitWrapper } from '../core/git-wrapper.js';
export default class List extends Command {
static description = 'List all hunks in a file with their line numbers';
static examples = [
'<%= config.bin %> <%= command.id %> # List all changed files',
'<%= config.bin %> <%= command.id %> src/index.ts # List hunks in specific file',
'<%= config.bin %> <%= command.id %> src/*.ts # List hunks in multiple files',
'<%= config.bin %> <%= command.id %> -p src/index.ts # Use precise mode for smaller hunks',
'<%= config.bin %> <%= command.id %> --preview # Show diff preview for each hunk',
];
static flags = {
precise: Flags.boolean({
char: 'p',
description: 'Use precise mode (U0 context) for finer control',
default: false,
}),
preview: Flags.boolean({
description: 'Show full diff preview for each hunk (legacy flag, now default behavior)',
default: false,
hidden: true,
}),
inline: Flags.boolean({
char: 'i',
description: 'Show inline summary with stats and first changed line',
default: false,
}),
oneline: Flags.boolean({
char: 'o',
description: 'Show only hunk headers without preview (like git log --oneline)',
default: false,
}),
context: Flags.integer({
char: 'c',
description: 'Number of context lines to show in preview',
default: 3,
}),
};
static args = {
files: Args.string({
description: 'Files to list hunks from (omit to show all)',
required: false,
}),
};
static strict = false; // Allow multiple files
async run() {
const { argv, flags } = await this.parse(List);
const precise = flags.precise;
if (process.env.DEBUG === '1') {
logger.setLevel(LogLevel.DEBUG);
}
try {
const git = new GitWrapper();
const staging = new StagingService(git.gitRoot);
const renderer = new DiffRenderer();
// Get files to process
let files;
if (argv.length === 0) {
// No files specified, get all changed files
files = await git.getChangedFiles();
if (files.length === 0) {
this.log(chalk.yellow('No changes found in repository'));
return;
}
}
else {
// Use specified files
files = argv;
}
// New behavior: show preview by default unless --oneline is used
// Legacy --preview flag is also respected if explicitly used
const showPreview = flags.preview || (!flags.oneline && !flags.inline);
const showInline = flags.inline || (flags.oneline && !flags.preview);
let hasChanges = false;
for (const file of files) {
try {
const hunks = await staging.listHunksWithInfo(file, { precise });
if (hunks.length === 0) {
continue;
}
hasChanges = true;
// Show file header
this.log(chalk.bold.blue(`\n${file}:`));
hunks.forEach((hunk) => {
// Format: [index|id] header (stats) | summary
let line = chalk.green(` [${hunk.index}|${hunk.id}] ${hunk.header}`);
if (showInline) {
const summary = renderer.renderHunkSummary(hunk);
if (summary) {
line += ' ' + summary;
}
}
this.log(line);
if (showPreview) {
const preview = renderer.renderHunk(hunk, { context: flags.context });
if (preview) {
const indentedPreview = preview.split('\n').map(l => ' ' + l).join('\n');
this.log(indentedPreview);
this.log(''); // Empty line between hunks
}
}
});
}
catch (error) {
// Skip files that can't be processed (e.g., binary files)
if (process.env.DEBUG === '1') {
logger.debug(`Skipping ${file}: ${error}`);
}
}
}
if (!hasChanges) {
this.log(chalk.yellow('No changes found in specified files'));
return;
}
this.log('');
this.log(chalk.dim('─'.repeat(60)));
this.log(`Use: ${chalk.cyan(`${this.config.bin} hunk <file>:<number|id>`)} to stage a specific hunk`);
this.log(` ${chalk.cyan(`${this.config.bin} hunk <file> <number|id>`)} (original syntax)`);
if (!precise) {
this.log(`\nTip: Use ${chalk.yellow('-p')} or ${chalk.yellow('--precise')} for more granular hunks`);
}
}
catch (error) {
logger.error(error instanceof Error ? error.message : String(error));
this.exit(1);
}
}
}