Update README.md and bin/gims.js with latest changes
This commit is contained in:
@@ -34,28 +34,29 @@ g o # AI analyzes changes, commits with perfect message, and pushes!
|
|||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
### 🤖 **AI-Powered Commit Messages**
|
### 🤖 **AI-Powered Commit Messages**
|
||||||
- **OpenAI GPT-4** integration for intelligent commit message generation
|
- OpenAI, Google Gemini, and Groq support with automatic provider selection
|
||||||
- **Google Gemini** support for lightning-fast analysis
|
|
||||||
- Smart diff analysis that understands your code changes
|
- Smart diff analysis that understands your code changes
|
||||||
- Handles large codebases with intelligent summarization
|
- Handles large codebases with intelligent summarization and safe truncation
|
||||||
|
- Optional Conventional Commits formatting and optional commit body generation (`--conventional`, `--body`)
|
||||||
|
|
||||||
### ⚡ **Lightning Fast Workflow**
|
### ⚡ **Lightning Fast Workflow**
|
||||||
- **One command commits**: `g o` - analyze, commit, and push in seconds
|
- One command commits: `g o` - analyze, commit, and push
|
||||||
- **Smart suggestions**: `g s` - get AI-generated messages copied to clipboard
|
- Smart suggestions: `g s` - get AI-generated messages copied to clipboard
|
||||||
- **Local commits**: `g l` - commit locally with AI messages
|
- Local commits: `g l` - commit locally with AI messages
|
||||||
- **Instant setup**: `g i` - initialize repos in a flash
|
- Staged-only by default for suggestions for precise control (use `--all` to stage everything)
|
||||||
|
|
||||||
### 🧠 **Intelligent Code Analysis**
|
### 🧠 **Intelligent Code Analysis**
|
||||||
- Analyzes actual code changes, not just file names
|
- Analyzes actual code changes, not just file names
|
||||||
- Understands context from function changes, imports, and logic
|
- Understands context from function changes, imports, and logic
|
||||||
- Handles everything from bug fixes to feature additions
|
- Graceful fallbacks for extremely large changesets and offline use
|
||||||
- Graceful fallbacks for extremely large changesets
|
|
||||||
|
|
||||||
### 🛠️ **Developer-Friendly**
|
### 🛠️ **Developer-Friendly**
|
||||||
- **Numbered commit history**: Easy navigation with `g ls`
|
- Numbered commit history with `g ls` / `g ll` and index-aware commands
|
||||||
- **Smart branching**: `g b 5` creates branch from commit #5
|
- Smart branching: `g b 5` creates branch from commit #5
|
||||||
- **Safe operations**: Built-in error handling and validation
|
- Safe operations with confirmations and dry-run support
|
||||||
- **Clean interface**: Intuitive commands that just make sense
|
- JSON output for editor integrations
|
||||||
|
- Quality-of-life: `--amend`, `undo` command, and automatic upstream setup on push
|
||||||
|
- Manual commit command for custom messages: `g m "your message"`
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
@@ -67,16 +68,25 @@ npm install -g gims
|
|||||||
|
|
||||||
### Setup AI (Choose One)
|
### Setup AI (Choose One)
|
||||||
|
|
||||||
**Option 1: OpenAI (Recommended)**
|
**Option 1: OpenAI**
|
||||||
```bash
|
```bash
|
||||||
export OPENAI_API_KEY="your-api-key-here"
|
export OPENAI_API_KEY="your-api-key-here"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2: Google Gemini (Faster)**
|
**Option 2: Google Gemini**
|
||||||
```bash
|
```bash
|
||||||
export GEMINI_API_KEY="your-api-key-here"
|
export GEMINI_API_KEY="your-api-key-here"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Option 3: Groq**
|
||||||
|
```bash
|
||||||
|
export GROQ_API_KEY="your-api-key-here"
|
||||||
|
# Optional, if self-hosting/proxying
|
||||||
|
export GROQ_BASE_URL="https://api.groq.com/openai/v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
GIMS auto-detects configured providers. If none are configured, it uses a local heuristic to generate sensible messages.
|
||||||
|
|
||||||
### Your First AI Commit
|
### Your First AI Commit
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -94,15 +104,33 @@ g o
|
|||||||
|---------|-------|-------------|---------|
|
|---------|-------|-------------|---------|
|
||||||
| `gims init` | `g i` | Initialize new Git repo | `g i` |
|
| `gims init` | `g i` | Initialize new Git repo | `g i` |
|
||||||
| `gims clone <repo>` | `g c` | Clone repository | `g c https://github.com/user/repo` |
|
| `gims clone <repo>` | `g c` | Clone repository | `g c https://github.com/user/repo` |
|
||||||
| `gims suggest` | `g s` | Generate & copy commit message | `g s` |
|
| `gims suggest` | `g s` | Generate & copy commit message from staged changes (use `--all` to stage) | `g s --all` |
|
||||||
| `gims local` | `g l` | AI commit locally | `g l` |
|
| `gims local` | `g l` | AI commit locally | `g l` |
|
||||||
| `gims online` | `g o` | AI commit + push | `g o` |
|
| `gims online` | `g o` | AI commit + push (use `--set-upstream` on first push) | `g o --set-upstream` |
|
||||||
|
| `gims commit <message...>` | `g m` | Commit with a custom message (no AI) | `g m "fix: handle empty input"` |
|
||||||
| `gims pull` | `g p` | Pull latest changes | `g p` |
|
| `gims pull` | `g p` | Pull latest changes | `g p` |
|
||||||
| `gims list` | `g ls` | Show numbered commit history | `g ls` |
|
| `gims list` | `g ls` | Show numbered commit history | `g ls` |
|
||||||
| `gims largelist` | `g ll` | Detailed commit history | `g ll` |
|
| `gims largelist` | `g ll` | Detailed commit history | `g ll` |
|
||||||
| `gims branch <n>` | `g b` | Branch from commit #n | `g b 3 feature-x` |
|
| `gims branch <n>` | `g b` | Branch from commit #n | `g b 3 feature-x` |
|
||||||
| `gims reset <n>` | `g r` | Reset to commit #n | `g r 5 --hard` |
|
| `gims reset <n>` | `g r` | Reset to commit #n (`--hard` needs `--yes`) | `g r 5 --hard --yes` |
|
||||||
| `gims revert <n>` | `g rv` | Safely revert commit #n | `g rv 2` |
|
| `gims revert <n>` | `g rv` | Safely revert commit #n (requires `--yes`) | `g rv 2 --yes` |
|
||||||
|
| `gims undo` | `g u` | Undo last commit (soft reset by default) | `g u` or `g u --hard --yes` |
|
||||||
|
|
||||||
|
### Global Options
|
||||||
|
|
||||||
|
- `--provider <name>`: AI provider: `auto` | `openai` | `gemini` | `groq` | `none`
|
||||||
|
- `--model <name>`: Override model identifier for the chosen provider
|
||||||
|
- `--staged-only`: Use only staged changes (default behavior for `g s`)
|
||||||
|
- `--all`: Stage all changes before running
|
||||||
|
- `--no-clipboard`: Do not copy suggestion to clipboard (for `g s`)
|
||||||
|
- `--body`: Generate a commit body in addition to subject
|
||||||
|
- `--conventional`: Format subject using Conventional Commits
|
||||||
|
- `--dry-run`: Print what would happen without committing/pushing
|
||||||
|
- `--verbose`: Verbose logging
|
||||||
|
- `--json`: Machine-readable output for `g s`
|
||||||
|
- `--yes`: Confirm destructive actions without prompting (e.g., reset/revert/undo)
|
||||||
|
- `--amend`: Amend the last commit instead of creating a new one
|
||||||
|
- `--set-upstream`: On push, set upstream if the current branch has none
|
||||||
|
|
||||||
## 💡 Real-World Examples
|
## 💡 Real-World Examples
|
||||||
|
|
||||||
@@ -136,50 +164,71 @@ g o
|
|||||||
|
|
||||||
## 🔥 Pro Tips
|
## 🔥 Pro Tips
|
||||||
|
|
||||||
### 🎯 **Perfect Workflow**
|
### 🎯 Perfect Workflow
|
||||||
```bash
|
```bash
|
||||||
# Daily development cycle
|
g p # Pull latest changes
|
||||||
g p # Pull latest changes
|
|
||||||
# ... code your features ...
|
# ... code your features ...
|
||||||
g s # Preview AI suggestion
|
g s # Preview AI suggestion from staged changes
|
||||||
g l # Commit locally first
|
g s --all # Or stage everything and suggest
|
||||||
|
g l # Commit locally first
|
||||||
# ... test your changes ...
|
# ... test your changes ...
|
||||||
g push # Push when ready
|
g o --set-upstream # Push with automatic upstream setup on first push
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🧠 **Smart Branching**
|
### 🧠 Smart Branching
|
||||||
```bash
|
```bash
|
||||||
g ls # See numbered history
|
g ls # See numbered history
|
||||||
g b 5 hotfix # Branch from commit #5
|
g b 5 hotfix # Branch from commit #5
|
||||||
g l # Make changes and commit
|
g l # Make changes and commit
|
||||||
g checkout main && g pull # Back to main
|
g checkout main && g pull # Back to main
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🛡️ **Safe Experimentation**
|
### 🛡️ Safe Experimentation
|
||||||
```bash
|
```bash
|
||||||
g l # Commit your experiment
|
g l # Commit your experiment
|
||||||
# ... code breaks something ...
|
# ... code breaks something ...
|
||||||
g r 1 --soft # Soft reset to previous commit
|
g r 1 --soft --yes # Soft reset to previous commit (confirmed)
|
||||||
# ... fix and try again ...
|
# ... or ...
|
||||||
|
g u --yes # Undo last commit (soft)
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
| Variable | Purpose | Required |
|
| Variable | Purpose |
|
||||||
|----------|---------|----------|
|
|----------|---------|
|
||||||
| `OPENAI_API_KEY` | OpenAI API access | One of these |
|
| `OPENAI_API_KEY` | OpenAI API access |
|
||||||
| `GEMINI_API_KEY` | Google Gemini API access | One of these |
|
| `GEMINI_API_KEY` | Google Gemini API access |
|
||||||
|
| `GROQ_API_KEY` | Groq API access (OpenAI-compatible) |
|
||||||
|
| `GROQ_BASE_URL` | Groq API base URL (optional) |
|
||||||
|
| `GIMS_PROVIDER` | Default provider: `auto` | `openai` | `gemini` | `groq` | `none` |
|
||||||
|
| `GIMS_MODEL` | Default model identifier for provider |
|
||||||
|
| `GIMS_CONVENTIONAL` | `1` to enable Conventional Commits by default |
|
||||||
|
| `GIMS_COPY` | `0` to disable clipboard copying in `g s` by default |
|
||||||
|
|
||||||
|
### .gimsrc (optional)
|
||||||
|
|
||||||
|
Place a `.gimsrc` JSON file in your repo root or home directory to set defaults:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"provider": "auto",
|
||||||
|
"model": "gpt-4o-mini",
|
||||||
|
"conventional": true,
|
||||||
|
"copy": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Smart Fallbacks
|
### Smart Fallbacks
|
||||||
|
|
||||||
GIMS handles edge cases gracefully:
|
GIMS handles edge cases gracefully:
|
||||||
|
|
||||||
- **🔄 Large diffs**: Automatically switches to file summary mode
|
- 🔄 Large diffs: Automatically switches to summary or status view
|
||||||
- **📊 Massive changes**: Falls back to status-based analysis
|
- ✂️ Massive text: Truncates safely with informative context
|
||||||
- **🛜 No API key**: Uses sensible default messages
|
- 🛜 No API key: Uses a local heuristic that summarizes your changes
|
||||||
- **⚠️ API failures**: Graceful degradation with helpful errors
|
- ⚠️ API failures: Clear errors and local fallback so you keep moving
|
||||||
|
- 🔒 Privacy-first: Only sends diffs when you explicitly run AI features
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
@@ -225,10 +274,10 @@ mno7890 Fix memory leak in image processing pipeline
|
|||||||
|
|
||||||
## 📈 Stats
|
## 📈 Stats
|
||||||
|
|
||||||
- ⚡ **10x faster** commits than traditional Git workflow
|
- ⚡ Faster commits than traditional Git workflow
|
||||||
- 🎯 **95%+ accuracy** in commit message relevance
|
- 🎯 High accuracy in commit message relevance
|
||||||
- 📚 **Zero learning curve** - if you know Git, you know GIMS
|
- 📚 Zero learning curve - if you know Git, you know GIMS
|
||||||
- 🌍 **Works everywhere** - Mac, Windows, Linux, WSL
|
- 🌍 Works everywhere - Mac, Windows, Linux, WSL
|
||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
|
|||||||
+433
-109
@@ -9,10 +9,82 @@ const clipboard = require('clipboardy');
|
|||||||
const process = require('process');
|
const process = require('process');
|
||||||
const { OpenAI } = require('openai');
|
const { OpenAI } = require('openai');
|
||||||
const { GoogleGenAI } = require('@google/genai');
|
const { GoogleGenAI } = require('@google/genai');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
const git = simpleGit();
|
const git = simpleGit();
|
||||||
|
|
||||||
|
// Utility: ANSI colors without extra deps
|
||||||
|
const color = {
|
||||||
|
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
||||||
|
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
||||||
|
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
||||||
|
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
||||||
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load simple config from .gimsrc (JSON) in cwd or home and env vars
|
||||||
|
function loadConfig() {
|
||||||
|
const defaults = {
|
||||||
|
provider: process.env.GIMS_PROVIDER || 'auto', // auto | openai | gemini | groq | none
|
||||||
|
model: process.env.GIMS_MODEL || '',
|
||||||
|
conventional: !!(process.env.GIMS_CONVENTIONAL === '1'),
|
||||||
|
copy: process.env.GIMS_COPY !== '0',
|
||||||
|
};
|
||||||
|
const tryFiles = [
|
||||||
|
path.join(process.cwd(), '.gimsrc'),
|
||||||
|
path.join(process.env.HOME || process.cwd(), '.gimsrc'),
|
||||||
|
];
|
||||||
|
for (const fp of tryFiles) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(fp)) {
|
||||||
|
const txt = fs.readFileSync(fp, 'utf8');
|
||||||
|
const json = JSON.parse(txt);
|
||||||
|
return { ...defaults, ...json };
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore malformed config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpts() {
|
||||||
|
// Merge precedence: CLI > config > env handled in loadConfig
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const cli = program.opts();
|
||||||
|
return {
|
||||||
|
provider: cli.provider || cfg.provider,
|
||||||
|
model: cli.model || cfg.model,
|
||||||
|
stagedOnly: !!cli.stagedOnly,
|
||||||
|
all: !!cli.all,
|
||||||
|
noClipboard: !!cli.noClipboard || cfg.copy === false,
|
||||||
|
body: !!cli.body,
|
||||||
|
conventional: !!cli.conventional || cfg.conventional,
|
||||||
|
dryRun: !!cli.dryRun,
|
||||||
|
verbose: !!cli.verbose,
|
||||||
|
json: !!cli.json,
|
||||||
|
yes: !!cli.yes,
|
||||||
|
amend: !!cli.amend,
|
||||||
|
setUpstream: !!cli.setUpstream,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureRepo() {
|
||||||
|
const isRepo = await git.checkIsRepo();
|
||||||
|
if (!isRepo) {
|
||||||
|
console.error(color.red('Not a git repository (or any of the parent directories).'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(prefix, err) {
|
||||||
|
const msg = err && err.message ? err.message : String(err);
|
||||||
|
console.error(color.red(`${prefix}: ${msg}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Safe log: returns { all: [] } on empty repo
|
// Safe log: returns { all: [] } on empty repo
|
||||||
async function safeLog() {
|
async function safeLog() {
|
||||||
try {
|
try {
|
||||||
@@ -24,7 +96,8 @@ async function safeLog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up AI-generated commit message
|
// Clean up AI-generated commit message
|
||||||
function cleanCommitMessage(message) {
|
function cleanCommitMessage(message, { body = false } = {}) {
|
||||||
|
if (!message) return 'Update project code';
|
||||||
// Remove markdown code blocks and formatting
|
// Remove markdown code blocks and formatting
|
||||||
let cleaned = message
|
let cleaned = message
|
||||||
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
||||||
@@ -34,28 +107,98 @@ function cleanCommitMessage(message) {
|
|||||||
.replace(/^\s*#+\s*/gm, '') // Remove headers
|
.replace(/^\s*#+\s*/gm, '') // Remove headers
|
||||||
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold formatting
|
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold formatting
|
||||||
.replace(/\*(.*?)\*/g, '$1') // Remove italic formatting
|
.replace(/\*(.*?)\*/g, '$1') // Remove italic formatting
|
||||||
|
.replace(/[\u{1F300}-\u{1FAFF}]/gu, '') // strip most emojis
|
||||||
|
.replace(/[\t\r]+/g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Take only the first line if multiple lines exist
|
// If a body is allowed, split subject/body, otherwise keep first line only
|
||||||
const firstLine = cleaned.split('\n')[0].trim();
|
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
|
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
||||||
// Ensure it's not too long
|
if (subject.length === 0) subject = 'Update project code';
|
||||||
return firstLine.length > 72 ? firstLine.substring(0, 69) + '...' : firstLine;
|
// Enforce concise subject
|
||||||
|
if (subject.length > 72) subject = subject.substring(0, 69) + '...';
|
||||||
|
|
||||||
|
if (!body) return subject;
|
||||||
|
|
||||||
|
const bodyLines = lines.slice(1).filter(l => l.length > 0);
|
||||||
|
const bodyText = bodyLines.join('\n').trim();
|
||||||
|
return bodyText ? `${subject}\n\n${bodyText}` : subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estimate tokens (rough approximation: 1 token ≈ 4 characters)
|
// Estimate tokens (rough approximation: 1 token ≈ 4 characters)
|
||||||
function estimateTokens(text) {
|
function estimateTokens(text) {
|
||||||
return Math.ceil(text.length / 4);
|
return Math.ceil((text || '').length / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProvider(pref) {
|
||||||
|
// pref: auto|openai|gemini|groq|none
|
||||||
|
if (pref === 'none') return 'none';
|
||||||
|
if (pref === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
|
||||||
|
if (pref === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
||||||
|
if (pref === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
||||||
|
// auto
|
||||||
|
if (process.env.GEMINI_API_KEY) return 'gemini';
|
||||||
|
if (process.env.OPENAI_API_KEY) return 'openai';
|
||||||
|
if (process.env.GROQ_API_KEY) return 'groq';
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHumanReadableChanges(limitPerList = 10) {
|
||||||
|
try {
|
||||||
|
const status = await git.status();
|
||||||
|
const modified = status.modified.slice(0, limitPerList);
|
||||||
|
const created = status.created.slice(0, limitPerList);
|
||||||
|
const deleted = status.deleted.slice(0, limitPerList);
|
||||||
|
const renamed = status.renamed.map(r => `${r.from}→${r.to}`).slice(0, limitPerList);
|
||||||
|
const parts = [];
|
||||||
|
if (created.length) parts.push(`Added: ${created.join(', ')}`);
|
||||||
|
if (modified.length) parts.push(`Modified: ${modified.join(', ')}`);
|
||||||
|
if (deleted.length) parts.push(`Deleted: ${deleted.join(', ')}`);
|
||||||
|
if (renamed.length) parts.push(`Renamed: ${renamed.join(', ')}`);
|
||||||
|
return parts.join('\n');
|
||||||
|
} catch (_) {
|
||||||
|
return 'Multiple file changes.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function localHeuristicMessage(status, { conventional = false } = {}) {
|
||||||
|
const created = status.created.length;
|
||||||
|
const modified = status.modified.length;
|
||||||
|
const deleted = status.deleted.length;
|
||||||
|
const total = created + modified + deleted + status.renamed.length;
|
||||||
|
|
||||||
|
const listFew = (arr) => arr.slice(0, 3).join(', ') + (arr.length > 3 ? ` and ${arr.length - 3} more` : '');
|
||||||
|
|
||||||
|
let type = 'chore';
|
||||||
|
let subject = 'update files';
|
||||||
|
if (created > 0 && modified === 0 && deleted === 0) {
|
||||||
|
type = 'feat';
|
||||||
|
subject = created <= 3 ? `add ${listFew(status.created)}` : `add ${created} files`;
|
||||||
|
} else if (deleted > 0 && created === 0 && modified === 0) {
|
||||||
|
type = 'chore';
|
||||||
|
subject = deleted <= 3 ? `remove ${listFew(status.deleted)}` : `remove ${deleted} files`;
|
||||||
|
} else if (modified > 0 && created === 0 && deleted === 0) {
|
||||||
|
type = 'chore';
|
||||||
|
subject = modified <= 3 ? `update ${listFew(status.modified)}` : `update ${modified} files`;
|
||||||
|
} else if (created > 0 || deleted > 0 || modified > 0) {
|
||||||
|
type = 'chore';
|
||||||
|
subject = `update ${total} files`;
|
||||||
|
}
|
||||||
|
const msg = conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
||||||
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate commit message with multiple fallback strategies
|
// Generate commit message with multiple fallback strategies
|
||||||
async function generateCommitMessage(rawDiff) {
|
async function generateCommitMessage(rawDiff, options = {}) {
|
||||||
|
const { conventional = false, body = false, provider: prefProvider = 'auto', model = '', verbose = false } = options;
|
||||||
const MAX_TOKENS = 100000; // Conservative limit (well below 128k)
|
const MAX_TOKENS = 100000; // Conservative limit (well below 128k)
|
||||||
const MAX_CHARS = MAX_TOKENS * 4;
|
const MAX_CHARS = MAX_TOKENS * 4;
|
||||||
|
|
||||||
let content = rawDiff;
|
let content = rawDiff;
|
||||||
let strategy = 'full';
|
let strategy = 'full';
|
||||||
|
|
||||||
|
const logv = (m) => { if (verbose) console.log(color.cyan(`[gims] ${m}`)); };
|
||||||
|
|
||||||
// Strategy 1: Check if full diff is too large
|
// Strategy 1: Check if full diff is too large
|
||||||
if (estimateTokens(rawDiff) > MAX_TOKENS) {
|
if (estimateTokens(rawDiff) > MAX_TOKENS) {
|
||||||
strategy = 'summary';
|
strategy = 'summary';
|
||||||
@@ -78,13 +221,15 @@ async function generateCommitMessage(rawDiff) {
|
|||||||
const modified = status.modified.slice(0, 10);
|
const modified = status.modified.slice(0, 10);
|
||||||
const created = status.created.slice(0, 10);
|
const created = status.created.slice(0, 10);
|
||||||
const deleted = status.deleted.slice(0, 10);
|
const deleted = status.deleted.slice(0, 10);
|
||||||
|
const renamed = status.renamed.map(r => `${r.from}→${r.to}`).slice(0, 10);
|
||||||
|
|
||||||
content = [
|
content = [
|
||||||
modified.length > 0 ? `Modified: ${modified.join(', ')}` : '',
|
modified.length > 0 ? `Modified: ${modified.join(', ')}` : '',
|
||||||
created.length > 0 ? `Added: ${created.join(', ')}` : '',
|
created.length > 0 ? `Added: ${created.join(', ')}` : '',
|
||||||
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : ''
|
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : '',
|
||||||
|
renamed.length > 0 ? `Renamed: ${renamed.join(', ')}` : '',
|
||||||
].filter(Boolean).join('\n');
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
if (status.files.length > 30) {
|
if (status.files.length > 30) {
|
||||||
content += `\n... and ${status.files.length - 30} more files`;
|
content += `\n... and ${status.files.length - 30} more files`;
|
||||||
}
|
}
|
||||||
@@ -105,54 +250,81 @@ async function generateCommitMessage(rawDiff) {
|
|||||||
summary: 'Changes are large; using summary. Write a concise git commit message for these changes:',
|
summary: 'Changes are large; using summary. Write a concise git commit message for these changes:',
|
||||||
status: 'Many files changed. Write a concise git commit message based on these file changes:',
|
status: 'Many files changed. Write a concise git commit message based on these file changes:',
|
||||||
truncated: 'Large diff truncated. Write a concise git commit message for these changes:',
|
truncated: 'Large diff truncated. Write a concise git commit message for these changes:',
|
||||||
fallback: 'Write a concise git commit message for:'
|
fallback: 'Write a concise git commit message for:',
|
||||||
};
|
};
|
||||||
|
|
||||||
const prompt = `${prompts[strategy]}\n${content}`;
|
const style = conventional ? 'Use Conventional Commits (e.g., feat:, fix:, chore:) for the subject.' : 'Subject must be a single short line.';
|
||||||
|
const bodyInstr = body ? 'Provide a short subject line followed by an optional body separated by a blank line.' : 'Return only a short subject line without extra quotes.';
|
||||||
|
const prompt = `${prompts[strategy]}\n${content}\n\n${style} ${bodyInstr}`;
|
||||||
|
|
||||||
// Final safety check
|
// Final safety check
|
||||||
if (estimateTokens(prompt) > MAX_TOKENS) {
|
if (estimateTokens(prompt) > MAX_TOKENS) {
|
||||||
console.warn('Changes too large for AI analysis, using default message');
|
console.warn(color.yellow('Changes too large for AI analysis, using default message'));
|
||||||
return 'Update multiple files';
|
return cleanCommitMessage('Update multiple files', { body });
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = 'Update project code'; // Default fallback
|
let message = 'Update project code'; // Default fallback
|
||||||
|
const provider = resolveProvider(prefProvider);
|
||||||
|
logv(`strategy=${strategy}, provider=${provider}${model ? `, model=${model}` : ''}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.env.GEMINI_API_KEY) {
|
if (provider === 'gemini') {
|
||||||
const genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
const genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
||||||
const res = await genai.models.generateContent({
|
const res = await genai.models.generateContent({
|
||||||
model: 'gemini-2.0-flash',
|
model: model || 'gemini-2.0-flash',
|
||||||
contents: prompt
|
contents: prompt,
|
||||||
});
|
});
|
||||||
message = (await res.response.text()).trim();
|
message = (await res.response.text()).trim();
|
||||||
} else if (process.env.OPENAI_API_KEY) {
|
} else if (provider === 'openai') {
|
||||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||||
const res = await openai.chat.completions.create({
|
const res = await openai.chat.completions.create({
|
||||||
model: 'gpt-4o-mini',
|
model: model || 'gpt-4o-mini',
|
||||||
messages: [{ role: 'user', content: prompt }],
|
messages: [{ role: 'user', content: prompt }],
|
||||||
temperature: 0.5,
|
temperature: 0.3,
|
||||||
max_tokens: 100 // Limit response length
|
max_tokens: body ? 200 : 80,
|
||||||
});
|
});
|
||||||
message = res.choices[0].message.content.trim();
|
message = (res.choices[0] && res.choices[0].message && res.choices[0].message.content || '').trim();
|
||||||
|
} else if (provider === 'groq') {
|
||||||
|
// Use OpenAI-compatible API via baseURL
|
||||||
|
const groq = new OpenAI({ apiKey: process.env.GROQ_API_KEY, baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1' });
|
||||||
|
const res = await groq.chat.completions.create({
|
||||||
|
model: model || 'llama-3.1-8b-instant',
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
temperature: 0.3,
|
||||||
|
max_tokens: body ? 200 : 80,
|
||||||
|
});
|
||||||
|
message = (res.choices[0] && res.choices[0].message && res.choices[0].message.content || '').trim();
|
||||||
|
} else {
|
||||||
|
// Local heuristic fallback
|
||||||
|
const status = await git.status();
|
||||||
|
message = localHeuristicMessage(status, { conventional });
|
||||||
|
const human = await getHumanReadableChanges();
|
||||||
|
if (body) message = `${message}\n\n${human}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'context_length_exceeded') {
|
if (error && error.code === 'context_length_exceeded') {
|
||||||
console.warn('Content still too large for AI, using default message');
|
console.warn(color.yellow('Content still too large for AI, using default message'));
|
||||||
return 'Update multiple files';
|
return cleanCommitMessage('Update multiple files', { body });
|
||||||
}
|
}
|
||||||
console.warn('AI generation failed:', error.message);
|
console.warn(color.yellow(`AI generation failed: ${error && error.message ? error.message : error}`));
|
||||||
|
// fallback to local heuristic
|
||||||
|
const status = await git.status();
|
||||||
|
message = localHeuristicMessage(status, { conventional });
|
||||||
|
const human = await getHumanReadableChanges();
|
||||||
|
if (body) message = `${message}\n\n${human}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanCommitMessage(message);
|
return cleanCommitMessage(message, { body });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveCommit(input) {
|
async function resolveCommit(input) {
|
||||||
if (/^\d+$/.test(input)) {
|
if (/^\d+$/.test(input)) {
|
||||||
const { all } = await safeLog();
|
const { all } = await safeLog();
|
||||||
|
// Align with list/largelist which show oldest -> newest
|
||||||
|
const ordered = [...all].reverse();
|
||||||
const idx = Number(input) - 1;
|
const idx = Number(input) - 1;
|
||||||
if (idx < 0 || idx >= all.length) throw new Error('Index out of range');
|
if (idx < 0 || idx >= ordered.length) throw new Error('Index out of range');
|
||||||
return all[idx].hash;
|
return ordered[idx].hash;
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
@@ -162,144 +334,296 @@ async function hasChanges() {
|
|||||||
return status.files.length > 0;
|
return status.files.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
program.name('gims').alias('g').version('0.4.3');
|
program
|
||||||
|
.name('gims')
|
||||||
|
.alias('g')
|
||||||
|
.version('0.5.0')
|
||||||
|
.option('--provider <name>', 'AI provider: auto|openai|gemini|groq|none')
|
||||||
|
.option('--model <name>', 'Model identifier for provider')
|
||||||
|
.option('--staged-only', 'Use only staged changes (default for suggest)')
|
||||||
|
.option('--all', 'Stage all changes before running')
|
||||||
|
.option('--no-clipboard', 'Do not copy suggestions to clipboard')
|
||||||
|
.option('--body', 'Generate a commit body in addition to subject')
|
||||||
|
.option('--conventional', 'Format messages using Conventional Commits')
|
||||||
|
.option('--dry-run', 'Do not perform writes (no commit or push)')
|
||||||
|
.option('--verbose', 'Verbose logging')
|
||||||
|
.option('--json', 'JSON output for suggest')
|
||||||
|
.option('--yes', 'Assume yes for confirmations')
|
||||||
|
.option('--amend', 'Amend the last commit instead of creating a new one')
|
||||||
|
.option('--set-upstream', 'Set upstream on push if missing');
|
||||||
|
|
||||||
program.command('init').alias('i')
|
program.command('init').alias('i')
|
||||||
.description('Initialize a new Git repository')
|
.description('Initialize a new Git repository')
|
||||||
.action(async () => { await git.init(); console.log('Initialized repo.'); });
|
.action(async () => {
|
||||||
|
try { await git.init(); console.log('Initialized repo.'); }
|
||||||
|
catch (e) { handleError('Init error', e); }
|
||||||
|
});
|
||||||
|
|
||||||
program.command('clone <repo>').alias('c')
|
program.command('clone <repo>').alias('c')
|
||||||
.description('Clone a Git repository')
|
.description('Clone a Git repository')
|
||||||
.action(async (repo) => {
|
.action(async (repo) => {
|
||||||
try { await git.clone(repo); console.log(`Cloned ${repo}`); }
|
try { await git.clone(repo); console.log(`Cloned ${repo}`); }
|
||||||
catch (e) { console.error('Clone error:', e.message); }
|
catch (e) { handleError('Clone error', e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('suggest').alias('s')
|
program.command('suggest').alias('s')
|
||||||
.description('Suggest commit message and copy to clipboard')
|
.description('Suggest commit message and copy to clipboard')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
if (!(await hasChanges())) {
|
await ensureRepo();
|
||||||
return console.log('No changes to suggest.');
|
const opts = getOpts();
|
||||||
}
|
|
||||||
|
|
||||||
const { all } = await safeLog();
|
|
||||||
const isFirst = all.length === 0;
|
|
||||||
|
|
||||||
// Always add changes first
|
|
||||||
await git.add('.');
|
|
||||||
|
|
||||||
// Get the appropriate diff
|
|
||||||
const rawDiff = await git.diff(['--cached']);
|
|
||||||
|
|
||||||
if (!rawDiff.trim()) {
|
|
||||||
return console.log('No changes to suggest.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = await generateCommitMessage(rawDiff);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clipboard.writeSync(msg);
|
if (opts.all) {
|
||||||
console.log(`Suggested: "${msg}" (copied to clipboard)`);
|
await git.add('.');
|
||||||
} catch (error) {
|
}
|
||||||
console.log(`Suggested: "${msg}" (clipboard copy failed)`);
|
|
||||||
|
// Use staged changes only; do not auto-stage unless --all
|
||||||
|
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
||||||
|
if (!rawDiff.trim()) {
|
||||||
|
if (opts.all) {
|
||||||
|
console.log('No changes to suggest.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('No staged changes. Use --all to stage everything or stage files manually.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = await generateCommitMessage(rawDiff, opts);
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
const out = { message: msg };
|
||||||
|
console.log(JSON.stringify(out));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.noClipboard) {
|
||||||
|
try { clipboard.writeSync(msg); console.log(`Suggested: "${msg}" ${color.green('(copied to clipboard)')}`); }
|
||||||
|
catch (_) { console.log(`Suggested: "${msg}" ${color.yellow('(clipboard copy failed)')}`); }
|
||||||
|
} else {
|
||||||
|
console.log(`Suggested: "${msg}"`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
handleError('Suggest error', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('local').alias('l')
|
program.command('local').alias('l')
|
||||||
.description('AI-powered local commit')
|
.description('AI-powered local commit')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
if (!(await hasChanges())) {
|
await ensureRepo();
|
||||||
return console.log('No changes to commit.');
|
const opts = getOpts();
|
||||||
}
|
|
||||||
|
|
||||||
const { all } = await safeLog();
|
try {
|
||||||
const isFirst = all.length === 0;
|
if (!(await hasChanges()) && !opts.all) {
|
||||||
|
console.log('No changes to commit.');
|
||||||
// Always add changes first
|
return;
|
||||||
await git.add('.');
|
}
|
||||||
|
|
||||||
// Get the appropriate diff
|
if (opts.all) await git.add('.');
|
||||||
const rawDiff = await git.diff(['--cached']);
|
|
||||||
|
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
||||||
if (!rawDiff.trim()) {
|
if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
|
||||||
return console.log('No changes to commit.');
|
|
||||||
|
const msg = await generateCommitMessage(rawDiff, opts);
|
||||||
|
|
||||||
|
if (opts.dryRun) {
|
||||||
|
console.log(color.yellow('[dry-run] Would commit with message:'));
|
||||||
|
console.log(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.amend) {
|
||||||
|
await git.raw(['commit', '--amend', '-m', msg]);
|
||||||
|
} else {
|
||||||
|
await git.commit(msg);
|
||||||
|
}
|
||||||
|
console.log(`Committed locally: "${msg}"`);
|
||||||
|
} catch (e) {
|
||||||
|
handleError('Local commit error', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = await generateCommitMessage(rawDiff);
|
|
||||||
await git.commit(msg);
|
|
||||||
console.log(`Committed locally: "${msg}"`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('online').alias('o')
|
program.command('online').alias('o')
|
||||||
.description('AI commit + push')
|
.description('AI commit + push')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
if (!(await hasChanges())) {
|
await ensureRepo();
|
||||||
return console.log('No changes to commit.');
|
const opts = getOpts();
|
||||||
}
|
|
||||||
|
|
||||||
const { all } = await safeLog();
|
try {
|
||||||
const isFirst = all.length === 0;
|
if (!(await hasChanges()) && !opts.all) {
|
||||||
|
console.log('No changes to commit.');
|
||||||
// Always add changes first
|
return;
|
||||||
await git.add('.');
|
}
|
||||||
|
|
||||||
// Get the appropriate diff
|
if (opts.all) await git.add('.');
|
||||||
const rawDiff = await git.diff(['--cached']);
|
|
||||||
|
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
||||||
if (!rawDiff.trim()) {
|
if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
|
||||||
return console.log('No changes to commit.');
|
|
||||||
|
const msg = await generateCommitMessage(rawDiff, opts);
|
||||||
|
|
||||||
|
if (opts.dryRun) {
|
||||||
|
console.log(color.yellow('[dry-run] Would commit & push with message:'));
|
||||||
|
console.log(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.amend) {
|
||||||
|
await git.raw(['commit', '--amend', '-m', msg]);
|
||||||
|
} else {
|
||||||
|
await git.commit(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await git.push();
|
||||||
|
console.log(`Committed & pushed: "${msg}"`);
|
||||||
|
} catch (pushErr) {
|
||||||
|
const msgErr = pushErr && pushErr.message ? pushErr.message : String(pushErr);
|
||||||
|
if (/no upstream|set the remote as upstream|have no upstream/.test(msgErr)) {
|
||||||
|
// Try to set upstream if requested
|
||||||
|
if (opts.setUpstream) {
|
||||||
|
const branch = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
||||||
|
await git.push(['--set-upstream', 'origin', branch]);
|
||||||
|
console.log(`Committed & pushed (upstream set to origin/${branch}): "${msg}"`);
|
||||||
|
} else {
|
||||||
|
console.log(color.yellow('Current branch has no upstream. Use --set-upstream to set origin/<branch> automatically.'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw pushErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
handleError('Online commit error', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program.command('commit <message...>').alias('m')
|
||||||
|
.description('Commit with a custom message (no AI)')
|
||||||
|
.action(async (messageParts) => {
|
||||||
|
await ensureRepo();
|
||||||
|
const opts = getOpts();
|
||||||
|
|
||||||
|
try {
|
||||||
|
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.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.all) await git.add('.');
|
||||||
|
|
||||||
|
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
||||||
|
if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
|
||||||
|
|
||||||
|
if (opts.dryRun) {
|
||||||
|
console.log(color.yellow('[dry-run] Would commit with custom message:'));
|
||||||
|
console.log(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.amend) {
|
||||||
|
await git.raw(['commit', '--amend', '-m', msg]);
|
||||||
|
} else {
|
||||||
|
await git.commit(msg);
|
||||||
|
}
|
||||||
|
console.log(`Committed locally: "${msg}"`);
|
||||||
|
} catch (e) {
|
||||||
|
handleError('Commit error', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg = await generateCommitMessage(rawDiff);
|
|
||||||
await git.commit(msg);
|
|
||||||
await git.push();
|
|
||||||
console.log(`Committed & pushed: "${msg}"`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('pull').alias('p')
|
program.command('pull').alias('p')
|
||||||
.description('Pull latest changes')
|
.description('Pull latest changes')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
|
await ensureRepo();
|
||||||
try { await git.pull(); console.log('Pulled latest.'); }
|
try { await git.pull(); console.log('Pulled latest.'); }
|
||||||
catch (e) { console.error('Pull error:', e.message); }
|
catch (e) { handleError('Pull error', e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('list').alias('ls')
|
program.command('list').alias('ls')
|
||||||
.description('Short numbered git log (oldest → newest)')
|
.description('Short numbered git log (oldest → newest)')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const { all } = await safeLog();
|
await ensureRepo();
|
||||||
all.reverse().forEach((c, i) => console.log(`${i+1}. ${c.hash.slice(0,7)} ${c.message}`));
|
try {
|
||||||
|
const { all } = await safeLog();
|
||||||
|
[...all].reverse().forEach((c, i) => console.log(`${i+1}. ${c.hash.slice(0,7)} ${c.message}`));
|
||||||
|
} catch (e) { handleError('List error', e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('largelist').alias('ll')
|
program.command('largelist').alias('ll')
|
||||||
.description('Full numbered git log (oldest → newest)')
|
.description('Full numbered git log (oldest → newest)')
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const { all } = await safeLog();
|
await ensureRepo();
|
||||||
all.reverse().forEach((c, i) => {
|
try {
|
||||||
const date = new Date(c.date).toLocaleString();
|
const { all } = await safeLog();
|
||||||
console.log(`${i+1}. ${c.hash.slice(0,7)} | ${date} | ${c.author_name} → ${c.message}`);
|
[...all].reverse().forEach((c, i) => {
|
||||||
});
|
const date = new Date(c.date).toLocaleString();
|
||||||
|
console.log(`${i+1}. ${c.hash.slice(0,7)} | ${date} | ${c.author_name} → ${c.message}`);
|
||||||
|
});
|
||||||
|
} catch (e) { handleError('Largelist error', e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('branch <c> [name]').alias('b')
|
program.command('branch <c> [name]').alias('b')
|
||||||
.description('Branch from commit/index')
|
.description('Branch from commit/index')
|
||||||
.action(async (c, name) => {
|
.action(async (c, name) => {
|
||||||
|
await ensureRepo();
|
||||||
try { const sha = await resolveCommit(c); const br = name || `branch-${sha.slice(0,7)}`; await git.checkout(['-b', br, sha]); console.log(`Switched to branch ${br} at ${sha}`); }
|
try { const sha = await resolveCommit(c); const br = name || `branch-${sha.slice(0,7)}`; await git.checkout(['-b', br, sha]); console.log(`Switched to branch ${br} at ${sha}`); }
|
||||||
catch (e) { console.error('Branch error:', e.message); }
|
catch (e) { handleError('Branch error', e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('reset <c>').alias('r')
|
program.command('reset <c>').alias('r')
|
||||||
.description('Reset branch to commit/index')
|
.description('Reset branch to commit/index')
|
||||||
.option('--hard','hard reset')
|
.option('--hard','hard reset')
|
||||||
.action(async (c, opts) => {
|
.action(async (c, optsCmd) => {
|
||||||
try { const sha = await resolveCommit(c); const mode = opts.hard? '--hard':'--soft'; await git.raw(['reset', mode, sha]); console.log(`Reset (${mode}) to ${sha}`); }
|
await ensureRepo();
|
||||||
catch (e) { console.error('Reset error:', e.message); }
|
try {
|
||||||
|
const sha = await resolveCommit(c);
|
||||||
|
const mode = optsCmd.hard? '--hard':'--soft';
|
||||||
|
const opts = getOpts();
|
||||||
|
if (!opts.yes) {
|
||||||
|
console.log(color.yellow(`About to run: git reset ${mode} ${sha}. Use --yes to confirm.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await git.raw(['reset', mode, sha]);
|
||||||
|
console.log(`Reset (${mode}) to ${sha}`);
|
||||||
|
}
|
||||||
|
catch (e) { handleError('Reset error', e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command('revert <c>').alias('rv')
|
program.command('revert <c>').alias('rv')
|
||||||
.description('Revert commit/index safely')
|
.description('Revert commit/index safely')
|
||||||
.action(async (c) => {
|
.action(async (c) => {
|
||||||
try { const sha = await resolveCommit(c); await git.revert(sha); console.log(`Reverted ${sha}`); }
|
await ensureRepo();
|
||||||
catch (e) { console.error('Revert error:', e.message); }
|
try {
|
||||||
|
const sha = await resolveCommit(c);
|
||||||
|
const opts = getOpts();
|
||||||
|
if (!opts.yes) {
|
||||||
|
console.log(color.yellow(`About to run: git revert ${sha}. Use --yes to confirm.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await git.revert(sha);
|
||||||
|
console.log(`Reverted ${sha}`);
|
||||||
|
}
|
||||||
|
catch (e) { handleError('Revert error', e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
program.command('undo').alias('u')
|
||||||
|
.description('Undo last commit (soft reset to HEAD~1)')
|
||||||
|
.option('--hard', 'Hard reset instead (destructive)')
|
||||||
|
.action(async (cmd) => {
|
||||||
|
await ensureRepo();
|
||||||
|
try {
|
||||||
|
const mode = cmd.hard ? '--hard' : '--soft';
|
||||||
|
const opts = getOpts();
|
||||||
|
if (!opts.yes) {
|
||||||
|
console.log(color.yellow(`About to run: git reset ${mode} HEAD~1. Use --yes to confirm.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
await git.raw(['reset', mode, 'HEAD~1']);
|
||||||
|
console.log(`Reset (${mode}) to HEAD~1`);
|
||||||
|
} catch (e) { handleError('Undo error', e); }
|
||||||
});
|
});
|
||||||
|
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
Reference in New Issue
Block a user