test: update tests

This commit is contained in:
s41r4j
2026-06-09 02:18:17 +05:30
parent 807d5bcb20
commit a54d529f31
6 changed files with 414 additions and 344 deletions
+197 -110
View File
@@ -128,6 +128,24 @@ async function confirmCommit(message, isLocalHeuristic) {
});
}
function askQuestion(promptText) {
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(resolve => {
rl.question(promptText, answer => {
rl.close();
resolve(answer.trim());
});
});
}
async function confirmPrompt(message, defaultYes = true) {
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
const answer = (await askQuestion(`${message} ${suffix}: `)).toLowerCase();
if (answer === '') return defaultYes;
return answer === 'y' || answer === 'yes';
}
async function resolveCommit(input) {
if (/^\d+$/.test(input)) {
const { all } = await safeLog();
@@ -145,6 +163,25 @@ async function hasChanges() {
return status.files.length > 0;
}
// Returns staged diff string, auto-staging as needed, or null when nothing to commit.
async function getStagedDiff(opts) {
if (!(await hasChanges()) && !opts.all) return null;
if (opts.all) {
Progress.info('Staging all changes...');
await git.add('.');
}
let diff = await git.diff(['--cached', '--no-ext-diff']);
if (!diff.trim()) {
Progress.info('No staged changes found; staging all changes...');
await git.add('.');
diff = await git.diff(['--cached', '--no-ext-diff']);
}
return diff.trim() ? diff : null;
}
program
.name('gims')
.alias('g')
@@ -528,27 +565,12 @@ program.command('local').alias('l')
const opts = getOpts();
try {
if (!(await hasChanges()) && !opts.all) {
const rawDiff = await getStagedDiff(opts);
if (!rawDiff) {
Progress.warning('No changes to commit');
return;
}
if (opts.all) {
Progress.info('Staging all changes...');
await git.add('.');
}
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.info('No staged changes found; staging all changes...');
await git.add('.');
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.warning('No changes to commit');
return;
}
}
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
const result = await generateCommitMessage(rawDiff, opts);
if (opts.progressIndicators) Progress.stop('');
@@ -590,27 +612,12 @@ program.command('online').alias('o')
const opts = getOpts();
try {
if (!(await hasChanges()) && !opts.all) {
const rawDiff = await getStagedDiff(opts);
if (!rawDiff) {
Progress.warning('No changes to commit');
return;
}
if (opts.all) {
Progress.info('Staging all changes...');
await git.add('.');
}
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.info('No staged changes found; staging all changes...');
await git.add('.');
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.warning('No changes to commit');
return;
}
}
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
const result = await generateCommitMessage(rawDiff, opts);
if (opts.progressIndicators) Progress.stop('');
@@ -675,22 +682,12 @@ program.command('commit <message...>').alias('m')
const msg = (messageParts || []).join(' ').trim();
if (!msg) { console.log('Provide a commit message.'); return; }
if (!(await hasChanges()) && !opts.all) {
console.log('No changes to commit.');
const rawDiff = await getStagedDiff(opts);
if (!rawDiff) {
Progress.warning('No changes to commit');
return;
}
if (opts.all) await git.add('.');
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
// Auto-stage all changes by default when nothing is staged
console.log(color.yellow('No staged changes found; staging all changes (git add .).'));
await git.add('.');
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) { console.log('No changes to commit.'); return; }
}
if (opts.dryRun) {
console.log(color.yellow('[dry-run] Would commit with custom message:'));
console.log(msg);
@@ -722,16 +719,114 @@ program.command('pull')
program.command('push')
.description('Push commits to remote')
.action(async () => {
.option('--tags', 'Push all tags to remote')
.action(async (cmdOptions) => {
await ensureRepo();
try {
Progress.info('Pushing to remote...');
await git.push();
Progress.success('Pushed to remote');
if (cmdOptions.tags) {
Progress.info('Pushing tags to remote...');
await git.push(['--tags']);
Progress.success('Tags pushed to remote');
} else {
Progress.info('Pushing to remote...');
await git.push();
Progress.success('Pushed to remote');
}
}
catch (e) { handleError('Push error', e); }
});
program.command('mirror [url]')
.alias('mr')
.description('Manage extra push destinations for origin (g push fans out to all of them)')
.action(async (url) => {
await ensureRepo();
try {
const remotes = await git.getRemotes(true);
const origin = remotes.find(r => r.name === 'origin');
if (!origin) {
Progress.warning("No 'origin' remote configured");
return;
}
const primary = origin.refs.fetch;
const pushUrls = (await git.raw(['config', '--get-all', 'remote.origin.pushurl']).catch(() => ''))
.split('\n').map(s => s.trim()).filter(Boolean);
const mirrors = pushUrls.filter(u => u !== primary);
if (url) {
if (primary === url || pushUrls.includes(url)) {
Progress.warning('That URL is already registered for origin');
return;
}
Progress.info(`Verifying ${url}...`);
try {
await git.listRemote([url]);
} catch {
const proceed = await confirmPrompt(`Could not reach "${url}". Add it anyway?`, false);
if (!proceed) { Progress.info('Cancelled'); return; }
}
// Make the primary an explicit pushurl first so it isn't lost once we add others
if (pushUrls.length === 0) {
await git.raw(['remote', 'set-url', '--push', 'origin', primary]);
}
await git.raw(['remote', 'set-url', '--add', '--push', 'origin', url]);
Progress.success(`Added mirror: ${url}`);
Progress.info(`'g push' now pushes to ${mirrors.length + 2} destination(s)`);
return;
}
console.log(color.bold('\nPush destinations for origin:'));
console.log(` ${color.green('●')} ${primary} ${color.dim('(primary)')}`);
if (mirrors.length === 0) {
console.log(color.dim('\nNo mirrors configured.'));
console.log(color.dim(`Add one with: ${color.cyan('g mirror <url>')}`));
return;
}
mirrors.forEach(m => console.log(` ${color.cyan('○')} ${m} ${color.dim('(mirror)')}`));
// Single readline session for the whole remove flow — separate
// interfaces on the same stdin can drop input on later prompts.
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise(resolve => rl.question(q, a => resolve(a.trim())));
const confirm = async (message, defaultYes) => {
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
const answer = (await ask(`${message} ${suffix}: `)).toLowerCase();
if (answer === '') return defaultYes;
return answer === 'y' || answer === 'yes';
};
try {
const wantsRemove = await confirm('\nRemove a mirror?', false);
if (!wantsRemove) return;
let target = mirrors[0];
if (mirrors.length > 1) {
const answer = await ask(`Which mirror? [1-${mirrors.length}]: `);
const idx = parseInt(answer) - 1;
if (isNaN(idx) || idx < 0 || idx >= mirrors.length) {
Progress.warning('Invalid selection, cancelled');
return;
}
target = mirrors[idx];
}
const confirmDelete = await confirm(`Remove "${target}"?`, false);
if (!confirmDelete) { Progress.info('Cancelled'); return; }
await git.raw(['remote', 'set-url', '--delete', '--push', 'origin', target]);
Progress.success(`Removed mirror: ${target}`);
} finally {
rl.close();
}
} catch (e) {
handleError('Mirror error', e);
}
});
program.command('sync')
.description('Smart sync: pull + rebase/merge')
.option('--rebase', 'Use rebase instead of merge')
@@ -826,11 +921,12 @@ program.command('stash')
Progress.start('🤖 Generating stash description');
const diff = await git.diff();
const description = await aiProvider.generateCommitMessage(diff, {
const descResult = await aiProvider.generateCommitMessage(diff, {
conventional: false,
body: false
});
Progress.stop('');
const description = descResult.message || descResult;
await git.stash(['push', '-m', `WIP: ${description}`]);
Progress.success(`Stashed changes: "${description}"`);
@@ -923,28 +1019,28 @@ program.command('amend').alias('a')
}
});
async function printCommitLog(limit) {
const log = await git.log({ maxCount: limit });
const commits = [...log.all].reverse();
if (commits.length === 0) {
Progress.info('No commits found');
return;
}
commits.forEach((c, i) => {
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
});
if (log.all.length >= limit) {
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
}
}
program.command('list').alias('ls')
.description('Short numbered git log (oldest → newest)')
.option('--limit <n>', 'Limit number of commits', '20')
.action(async (cmdOptions) => {
await ensureRepo();
try {
const limit = parseInt(cmdOptions.limit) || 20;
const log = await git.log({ maxCount: limit });
const commits = [...log.all].reverse();
if (commits.length === 0) {
Progress.info('No commits found');
return;
}
commits.forEach((c, i) => {
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
});
if (log.all.length >= limit) {
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
}
await printCommitLog(parseInt(cmdOptions.limit) || 20);
} catch (e) {
handleError('List error', e);
}
@@ -984,22 +1080,7 @@ program.command('history').alias('h')
.action(async (cmdOptions) => {
await ensureRepo();
try {
const limit = parseInt(cmdOptions.limit) || 20;
const log = await git.log({ maxCount: limit });
const commits = [...log.all].reverse();
if (commits.length === 0) {
Progress.info('No commits found');
return;
}
commits.forEach((c, i) => {
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
});
if (log.all.length >= limit) {
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
}
await printCommitLog(parseInt(cmdOptions.limit) || 20);
} catch (e) {
handleError('History error', e);
}
@@ -1743,14 +1824,16 @@ program.command('fix').alias('f')
if (behind > 0) {
try {
const behindLog = await git.log({ from: branch, to: remoteBranch, maxCount: 5 });
// symmetric: false → "branch..remoteBranch" (commits on remote only, not "branch...remoteBranch" which mixes both directions)
const behindLog = await git.log({ from: branch, to: remoteBranch, symmetric: false, maxCount: 5 });
aiContext += `Incoming commits (latest 5):\n${behindLog.all.map(c => `- ${c.message}`).join('\n')}\n\n`;
} catch (e) { }
}
if (ahead > 0) {
try {
const aheadLog = await git.log({ from: remoteBranch, to: branch, maxCount: 5 });
// symmetric: false → "remoteBranch..branch" (commits on local only)
const aheadLog = await git.log({ from: remoteBranch, to: branch, symmetric: false, maxCount: 5 });
aiContext += `My outgoing commits (latest 5):\n${aheadLog.all.map(c => `- ${c.message}`).join('\n')}\n`;
} catch (e) { }
}
@@ -1766,36 +1849,40 @@ program.command('fix').alias('f')
Recommended Command: <command>
`;
// Use preferred provider or auto-resolve
const provider = aiProvider.resolveProvider(opts.provider);
if (provider === 'none') {
const heuristicRec = () => {
if (ahead > 0 && behind === 0) return 'Push (g push)';
if (behind > 0 && ahead === 0) return 'Pull (g pull)';
return 'Rebase (g fix --rebase)';
};
// Try each available provider in priority order, with full fallback chain
const providerChain = aiProvider.buildProviderChain(opts.provider || 'auto')
.filter(p => p !== 'local');
if (providerChain.length === 0) {
Progress.stop('');
console.log(color.yellow('No AI provider configured. Falling back to simple heuristics.'));
// ... heuristic fallback ...
let rec = '';
if (ahead > 0 && behind === 0) rec = 'Push (g push)';
else if (behind > 0 && ahead === 0) rec = 'Pull (g pull)';
else rec = 'Rebase (g fix --rebase)';
console.log(`Recommendation: ${rec}`);
console.log(`Recommendation: ${heuristicRec()}`);
return;
}
try {
const response = await aiProvider.generateWithProvider(provider, prompt, { temperature: 0.3 }); // Use generic provider method
Progress.stop('');
console.log(`\n${color.bold('🤖 AI Analysis:')}`);
console.log(response.trim());
} catch (e) {
Progress.stop('');
console.log(color.yellow('AI Analysis failed, falling back to heuristics.'));
// ... heuristic fallback code ...
let rec = '';
if (ahead > 0 && behind === 0) rec = 'Push (g push)';
else if (behind > 0 && ahead === 0) rec = 'Pull (g pull)';
else rec = 'Rebase (g fix --rebase)';
console.log(`Recommendation: ${rec}`);
let response = null;
for (const p of providerChain) {
try {
response = await aiProvider.generateWithProvider(p, prompt, { temperature: 0.3 });
break;
} catch { continue; }
}
Progress.stop('');
if (!response) {
console.log(color.yellow('AI providers unavailable. Falling back to heuristics.'));
console.log(`Recommendation: ${heuristicRec()}`);
return;
}
console.log(`\n${color.bold('🤖 AI Analysis:')}`);
console.log(response.trim());
return;
}