Compare commits
5 Commits
68427c4a11
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
3a57d25a76
|
|||
|
8152edba39
|
|||
|
679db5b1db
|
|||
|
b1ad68c7b5
|
|||
|
280d6ec5c9
|
12
Cargo.toml
12
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "quicommit"
|
||||
version = "0.1.11"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
authors = ["Sidney Zhang <zly@lyzhang.me>"]
|
||||
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
|
||||
@@ -83,11 +83,13 @@ mockall = "0.12"
|
||||
wiremock = "0.6"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = "s"
|
||||
lto = "thin"
|
||||
codegen-units = 2
|
||||
panic = "abort"
|
||||
strip = true
|
||||
debug = false
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
opt-level = 1
|
||||
debug = true
|
||||
|
||||
128
RAODMAP.md
Normal file
128
RAODMAP.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# QuiCommit Roadmap
|
||||
|
||||
## 已完成 ✅
|
||||
|
||||
- [x] 基础 Git 操作(commit、tag、changelog)
|
||||
- [x] AI 驱动提交信息生成(Conventional Commits / commitlint 格式)
|
||||
- [x] 多 LLM 提供商支持:Ollama、OpenAI、Anthropic、Kimi、DeepSeek、OpenRouter
|
||||
- [x] 多 Git Profile 管理(SSH 密钥 + GPG 签名)
|
||||
- [x] 语义化版本自动升级与 AI 发布说明
|
||||
- [x] Keep a Changelog 格式自动生成
|
||||
- [x] 系统密钥环安全存储 API Key
|
||||
- [x] 敏感数据加密存储(AES-GCM + Argon2)
|
||||
- [x] 交互式 CLI 预览与确认
|
||||
- [x] 7 种语言国际化支持
|
||||
- [x] 配置导出/导入(支持加密保护)
|
||||
- [x] Profile Token 管理(PAT 等)
|
||||
|
||||
---
|
||||
|
||||
## 进行中 🚧
|
||||
|
||||
暂无。
|
||||
|
||||
---
|
||||
|
||||
## 计划中 📋
|
||||
|
||||
### 1. Git 凭证管理器
|
||||
|
||||
将 Git 凭证管理集成到 QuiCommit 中,统一管理 HTTPS 仓库的身份认证。
|
||||
|
||||
- [ ] **Git Credential Helper 集成**
|
||||
- 实现 `git credential-store` / `git-credential-libsecret` 等标准的 credential helper 协议
|
||||
- 支持 `quicommit credential get|store|erase` 子命令
|
||||
- 与系统密钥环无缝对接,复用已有的 `KeyringManager`
|
||||
|
||||
- [ ] **凭证管理 CLI**
|
||||
- `quicommit credential list` — 列出所有已存储的凭证
|
||||
- `quicommit credential add` — 手动添加凭证(用户名 + 密码/Token)
|
||||
- `quicommit credential remove` — 删除指定凭证
|
||||
- `quicommit credential status` — 查看凭证管理状态
|
||||
|
||||
- [ ] **跨平台支持**
|
||||
- Windows:集成 Windows Credential Manager
|
||||
- macOS:集成 Keychain
|
||||
- Linux:通过 Secret Service / D-Bus 对接 GNOME Keyring / KWallet
|
||||
|
||||
- [ ] **安全增强**
|
||||
- 支持 PAT(Personal Access Token)按 scope / 有效期管理
|
||||
- 支持凭证过期检查和自动提醒
|
||||
|
||||
---
|
||||
|
||||
### 2. 新增模型支持
|
||||
|
||||
扩展 LLM 提供商和模型覆盖范围,满足更多场景和偏好。
|
||||
|
||||
- [ ] **新增 DeepSeek 最新模型**
|
||||
- 支持 `deepseek-chat`(DeepSeek-V3)
|
||||
- 支持 `deepseek-reasoner`(DeepSeek-R1)
|
||||
- 支持 `deepseek-v4`(待官方发布后跟进)
|
||||
|
||||
- [ ] **新增国内模型提供商**
|
||||
- 通义千问 (Qwen) — 阿里云 DashScope API
|
||||
- 文心一言 (ERNIE) — 百度千帆 API
|
||||
- 智谱 GLM — ChatGLM API
|
||||
- 百川 (Baichuan) — Baichuan API
|
||||
|
||||
- [ ] **新增国际模型提供商**
|
||||
- Google Gemini API
|
||||
- Mistral AI API
|
||||
- Cohere API
|
||||
- Groq (LPU 推理加速)
|
||||
|
||||
- [ ] **本地模型扩展**
|
||||
- 支持 llama.cpp 服务端(兼容 OpenAI API 格式)
|
||||
- 支持 vLLM 部署的模型
|
||||
- 本地模型推荐列表与一键配置向导
|
||||
|
||||
- [ ] **模型能力适配**
|
||||
- 不同模型的 token 限制自适应
|
||||
- 模型特定的 prompt 模板优化
|
||||
- 支持 function calling / tool use(用于复杂生成场景)
|
||||
|
||||
---
|
||||
|
||||
### 3. 生成体验优化
|
||||
|
||||
提升 AI 生成提交信息、标签说明和变更日志时的用户体验。
|
||||
|
||||
- [ ] **流式输出与实时反馈**
|
||||
- 支持 SSE(Server-Sent Events)流式生成
|
||||
- 终端打字机效果实时显示生成内容
|
||||
- 流式生成过程中支持 `Ctrl+C` 中断
|
||||
|
||||
- [ ] **生成质量提升**
|
||||
- 基于 commitlint 规则的后校验与自动修正
|
||||
- 支持 Few-shot 示例引导(用户可自定义示例库)
|
||||
- 生成结果的置信度评分与多候选方案
|
||||
|
||||
- [ ] **Diff 上下文增强**
|
||||
- 智能 diff 摘要(大改动时自动压缩关键信息)
|
||||
- 支持 `.gitattributes` 排除/包含规则
|
||||
- 按文件类型分组生成更精准的提交描述
|
||||
|
||||
- [ ] **交互式编辑增强**
|
||||
- 生成后支持内联编辑(类似 `git rebase -i` 体验)
|
||||
- 支持重新生成指定部分(如 scope、description)
|
||||
- 历史提交信息学习与风格适配
|
||||
|
||||
- [ ] **批量操作支持**
|
||||
- 批量生成多个 commit(分组暂存区变更)
|
||||
- `--dry-run` 预览模式(只生成本地查看,不写 Git)
|
||||
|
||||
- [ ] **性能优化**
|
||||
- API 请求并发优化(多个模型同时生成候选)
|
||||
- 本地缓存常用 prompt 模板
|
||||
- 减少不必要的 diff 计算
|
||||
|
||||
---
|
||||
|
||||
## 长远规划 🌟
|
||||
|
||||
- [ ] **VS Code 扩展** — 在 IDE 内直接使用 QuiCommit
|
||||
- [ ] **GitHub Action / GitLab CI 集成** — 自动化 PR 标题和描述生成
|
||||
- [ ] **团队协作** — 共享 commit 风格配置、prompt 模板库
|
||||
- [ ] **Web Dashboard** — 可视化管理多仓库的 Git 活动与 AI 生成统计
|
||||
- [ ] **插件系统** — 允许社区贡献自定义 LLM 提供商和生成策略
|
||||
@@ -47,7 +47,7 @@ impl ContentGenerator {
|
||||
format: CommitFormat,
|
||||
language: Language,
|
||||
) -> Result<GeneratedCommit> {
|
||||
let diff = repo.get_staged_diff()
|
||||
let diff = repo.get_staged_diff_sorted()
|
||||
.context("Failed to get staged diff")?;
|
||||
|
||||
if diff.is_empty() {
|
||||
@@ -116,7 +116,7 @@ impl ContentGenerator {
|
||||
) -> Result<GeneratedCommit> {
|
||||
use dialoguer::Select;
|
||||
|
||||
let diff = repo.get_staged_diff()?;
|
||||
let diff = repo.get_staged_diff_sorted()?;
|
||||
|
||||
if diff.is_empty() {
|
||||
anyhow::bail!("No staged changes");
|
||||
|
||||
@@ -159,7 +159,7 @@ impl ChangelogGenerator {
|
||||
let mut output = format!("## [{}] - {}\n\n", version, date_str);
|
||||
|
||||
if self.group_by_type {
|
||||
let grouped = self.group_commits(commits);
|
||||
let _grouped = self.group_commits(commits);
|
||||
|
||||
// Standard categories
|
||||
let categories = vec![
|
||||
@@ -230,7 +230,7 @@ impl ChangelogGenerator {
|
||||
|
||||
fn generate_github_releases(
|
||||
&self,
|
||||
version: &str,
|
||||
_version: &str,
|
||||
_date: DateTime<Utc>,
|
||||
commits: &[CommitInfo],
|
||||
) -> Result<String> {
|
||||
|
||||
115
src/git/mod.rs
115
src/git/mod.rs
@@ -8,8 +8,6 @@ pub mod changelog;
|
||||
pub mod commit;
|
||||
pub mod tag;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
|
||||
fn normalize_path_for_git2(path: &Path) -> PathBuf {
|
||||
let mut normalized = path.to_path_buf();
|
||||
@@ -346,6 +344,119 @@ impl GitRepo {
|
||||
Ok(diff_text)
|
||||
}
|
||||
|
||||
/// Get staged diff with files sorted by importance
|
||||
/// Important files (source code) come first, then config files like Cargo.toml,
|
||||
/// then lock files like Cargo.lock
|
||||
pub fn get_staged_diff_sorted(&self) -> Result<String> {
|
||||
let diff = self.get_staged_diff()?;
|
||||
|
||||
if diff.is_empty() {
|
||||
return Ok(diff);
|
||||
}
|
||||
|
||||
let mut file_diffs = Vec::new();
|
||||
let mut current_file_diff = String::new();
|
||||
let mut current_file = String::new();
|
||||
|
||||
for line in diff.lines() {
|
||||
if line.starts_with("diff --git") {
|
||||
// Save previous file diff if any
|
||||
if !current_file_diff.is_empty() && !current_file.is_empty() {
|
||||
file_diffs.push((current_file.clone(), current_file_diff.clone()));
|
||||
}
|
||||
current_file = extract_file_from_diff_line(line);
|
||||
current_file_diff = format!("{}\n", line);
|
||||
} else {
|
||||
current_file_diff.push_str(line);
|
||||
current_file_diff.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last file diff
|
||||
if !current_file_diff.is_empty() && !current_file.is_empty() {
|
||||
file_diffs.push((current_file, current_file_diff));
|
||||
}
|
||||
|
||||
// Sort by file importance
|
||||
file_diffs.sort_by(|a, b| {
|
||||
let score_a = file_importance_score(&a.0);
|
||||
let score_b = file_importance_score(&b.0);
|
||||
score_b.cmp(&score_a) // Descending order
|
||||
});
|
||||
|
||||
// Combine sorted diffs
|
||||
let sorted_diff: String = file_diffs.into_iter()
|
||||
.map(|(_, diff)| diff)
|
||||
.collect();
|
||||
|
||||
Ok(sorted_diff)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract filename from diff --git line
|
||||
fn extract_file_from_diff_line(line: &str) -> String {
|
||||
// Format: "diff --git a/path/to/file b/path/to/file"
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
// Return the second path (after b/)
|
||||
if let Some(path) = parts[2].strip_prefix("b/") {
|
||||
return path.to_string();
|
||||
}
|
||||
// Fallback to first path (after a/)
|
||||
if let Some(path) = parts[1].strip_prefix("a/") {
|
||||
return path.to_string();
|
||||
}
|
||||
}
|
||||
line.to_string()
|
||||
}
|
||||
|
||||
/// Calculate file importance score
|
||||
/// Higher score = more important
|
||||
fn file_importance_score(filename: &str) -> i32 {
|
||||
// Priority list for important file types
|
||||
let important_extensions = [
|
||||
".rs", ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".java", ".cpp", ".c", ".rust",
|
||||
".vue", ".svelte", ".html", ".css", ".scss", ".sass", ".less",
|
||||
];
|
||||
|
||||
// Config files that are important but less than source code
|
||||
let config_files = [
|
||||
"Cargo.toml", "package.json", "go.mod", "go.sum", "pom.xml",
|
||||
"Makefile", "CMakeLists.txt", "build.gradle", "gradle.properties",
|
||||
];
|
||||
|
||||
// Lock files - lowest priority
|
||||
let lock_files = [
|
||||
"Cargo.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
||||
"Gemfile.lock", "composer.lock",
|
||||
];
|
||||
|
||||
// Check lock files first (lowest priority)
|
||||
for lock in lock_files.iter() {
|
||||
if filename.ends_with(lock) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check config files (medium priority)
|
||||
for config in config_files.iter() {
|
||||
if filename.ends_with(config) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Check important source files (highest priority)
|
||||
for ext in important_extensions.iter() {
|
||||
if filename.ends_with(ext) {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Default priority for other files
|
||||
2
|
||||
}
|
||||
|
||||
impl GitRepo {
|
||||
/// Get unstaged diff
|
||||
pub fn get_unstaged_diff(&self) -> Result<String> {
|
||||
let diff = self.repo.diff_index_to_workdir(None, None)?;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -41,10 +41,10 @@ pub fn get_editor() -> String {
|
||||
.or_else(|_| std::env::var("VISUAL"))
|
||||
.unwrap_or_else(|_| {
|
||||
if cfg!(target_os = "windows") {
|
||||
if let Ok(code) = which::which("code") {
|
||||
if let Ok(_code) = which::which("code") {
|
||||
return "code --wait".to_string();
|
||||
}
|
||||
if let Ok(notepad) = which::which("notepad") {
|
||||
if let Ok(_notepad) = which::which("notepad") {
|
||||
return "notepad".to_string();
|
||||
}
|
||||
"notepad".to_string()
|
||||
|
||||
Reference in New Issue
Block a user