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
JavaScript
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): ');
}