UNPKG

reviewit

Version:

A lightweight command-line tool that spins up a local web server to display Git commit diffs in a GitHub-like Files changed view

170 lines (169 loc) 7.28 kB
#!/usr/bin/env node import { Command } from 'commander'; import React from 'react'; import { simpleGit } from 'simple-git'; import pkg from '../../package.json' with { type: 'json' }; import { startServer } from '../server/server.js'; import { findUntrackedFiles, markFilesIntentToAdd, promptUser, validateDiffArguments, resolvePrCommits, } from './utils.js'; function isSpecialArg(arg) { return arg === 'working' || arg === 'staged' || arg === '.'; } const program = new Command(); program .name('reviewit') .description('A lightweight Git diff viewer with GitHub-like interface') .version(pkg.version) .argument('[commit-ish]', 'Git commit, tag, branch, HEAD~n reference, or "working"/"staged"/"."', 'HEAD') .argument('[compare-with]', 'Optional: Compare with this commit/branch (shows diff between commit-ish and compare-with)') .option('--port <port>', 'preferred port (auto-assigned if occupied)', parseInt) .option('--host <host>', 'host address to bind', '127.0.0.1') .option('--no-open', 'do not automatically open browser') .option('--mode <mode>', 'diff mode (side-by-side or inline)', 'side-by-side') .option('--tui', 'use terminal UI instead of web interface') .option('--pr <url>', 'GitHub PR URL to review (e.g., https://github.com/owner/repo/pull/123)') .action(async (commitish, compareWith, options) => { try { // Determine target and base commitish let targetCommitish = commitish; let baseCommitish; // Handle PR URL option if (options.pr) { if (commitish !== 'HEAD' || compareWith) { console.error('Error: --pr option cannot be used with positional arguments'); process.exit(1); } try { const prCommits = await resolvePrCommits(options.pr); targetCommitish = prCommits.targetCommitish; baseCommitish = prCommits.baseCommitish; console.log(`📋 Reviewing PR: ${options.pr}`); console.log(`🎯 Target commit: ${targetCommitish.substring(0, 7)}`); console.log(`📍 Base commit: ${baseCommitish.substring(0, 7)}`); } catch (error) { console.error(`Error resolving PR: ${error instanceof Error ? error.message : 'Unknown error'}`); process.exit(1); } } else if (compareWith) { // If compareWith is provided, use it as base baseCommitish = compareWith; } else { // Handle special arguments if (commitish === 'working') { // working compares working directory with staging area baseCommitish = 'staged'; } else if (isSpecialArg(commitish)) { baseCommitish = 'HEAD'; } else { baseCommitish = commitish + '^'; } } if (commitish === 'working' || commitish === '.') { const git = simpleGit(); await handleUntrackedFiles(git); } if (options.tui) { // Check if we're in a TTY environment if (!process.stdin.isTTY) { console.error('Error: TUI mode requires an interactive terminal (TTY).'); console.error('Try running the command directly in your terminal without piping.'); process.exit(1); } // Dynamic import for TUI mode const { render } = await import('ink'); const { default: TuiApp } = await import('../tui/App.js'); render(React.createElement(TuiApp, { targetCommitish, baseCommitish, mode: options.mode })); return; } // Skip validation for PR URLs as they're already resolved to valid commits if (!options.pr) { const validation = validateDiffArguments(targetCommitish, compareWith); if (!validation.valid) { console.error(`Error: ${validation.error}`); process.exit(1); } } const { url, port, isEmpty } = await startServer({ targetCommitish, baseCommitish, preferredPort: options.port, host: options.host, openBrowser: options.open, mode: options.mode, }); console.log(`\n🚀 ReviewIt server started on ${url}`); console.log(`📋 Reviewing: ${targetCommitish}`); if (isEmpty) { console.log('\n! \x1b[33mNo differences found. Browser will not open automatically.\x1b[0m'); console.log(` Server is running at ${url} if you want to check manually.\n`); } else if (options.open) { console.log('🌐 Opening browser...\n'); } else { console.log('💡 Use --open to automatically open browser\n'); } process.on('SIGINT', async () => { console.log('\n👋 Shutting down ReviewIt server...'); // Try to fetch comments before shutting down try { const response = await fetch(`http://localhost:${port}/api/comments-output`); if (response.ok) { const data = await response.text(); if (data.trim()) { console.log(data); } } } catch { // Silently ignore fetch errors during shutdown } process.exit(0); }); } catch (error) { console.error('Error:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } }); // Show deprecation message showDeprecationMessage(); program.parse(); function showDeprecationMessage() { const message = 'reviewit is deprecated. Please use difit instead: '; const command = 'npx difit'; const fullMessage = message + command; const border = '═'.repeat(fullMessage.length + 4); // Red border, yellow text with bold command console.log(`\n\x1b[31m╔${border}╗\x1b[0m`); console.log(`\x1b[31m║\x1b[0m \x1b[33m${message}\x1b[1m${command}\x1b[0m \x1b[31m║\x1b[0m`); console.log(`\x1b[31m╚${border}╝\x1b[0m\n`); } // Check for untracked files and prompt user to add them for diff visibility async function handleUntrackedFiles(git) { const files = await findUntrackedFiles(git); if (files.length === 0) { return; } const userConsent = await promptUserToIncludeUntracked(files); if (userConsent) { await markFilesIntentToAdd(git, files); console.log('✅ Files added with --intent-to-add'); const filesAsArgs = files.join(' '); console.log(` 💡 To undo this, run \`git reset -- ${filesAsArgs}\``); } else { console.log('i Untracked files will not be shown in diff'); } } async function promptUserToIncludeUntracked(files) { console.log(`\n📝 Found ${files.length} untracked file(s):`); for (const file of files) { console.log(` - ${file}`); } return await promptUser('\n❓ Would you like to include these untracked files in the diff review? (Y/n): '); }