test: update tests
This commit is contained in:
+197
-110
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user