2 Commits

7 changed files with 304 additions and 47 deletions

View File

@@ -1,4 +1,11 @@
## v1.0.1 更新日志 # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.4] - 2026-02-01
### ✨ 新功能 ### ✨ 新功能
- 新增 `test3.txt`,支持中文输出测试 - 新增 `test3.txt`,支持中文输出测试
@@ -20,3 +27,20 @@
### 🔧 其他变更 ### 🔧 其他变更
- 新增个人访问令牌、使用统计与配置校验功能 - 新增个人访问令牌、使用统计与配置校验功能
- 添加 `test2.txt` 占位文件 - 添加 `test2.txt` 占位文件
## [0.1.0] - 2026-01-30
### Added
- Initial project structure
- Core functionality for git operations
- LLM integration
- Configuration management
- CLI interface
### Features
- **Commit Generation**: Automatically generate conventional commit messages from git diffs
- **Profile Management**: Switch between multiple Git identities for different contexts
- **Tag Management**: Create annotated tags with AI-generated release notes
- **Changelog**: Generate and maintain changelog in Keep a Changelog format
- **Security**: Encrypt SSH passphrases and API keys
- **Interactive UI**: Beautiful CLI with prompts and previews

View File

@@ -877,6 +877,105 @@ impl ConfigCommand {
bail!("OpenAI API key not configured"); bail!("OpenAI API key not configured");
} }
} }
"anthropic" => {
if let Some(ref key) = config.llm.anthropic.api_key {
let client = crate::llm::AnthropicClient::new(
key,
&config.llm.anthropic.model,
)?;
println!("Fetching available models from Anthropic...");
match client.list_models().await {
Ok(models) => {
println!("\n{}", "Available models:".bold());
for model in models {
let marker = if model == config.llm.anthropic.model { "".green() } else { "".dimmed() };
println!("{} {}", marker, model);
}
}
Err(e) => {
println!("{} Failed to fetch models: {}", "".red(), e);
}
}
} else {
bail!("Anthropic API key not configured");
}
}
"kimi" => {
if let Some(ref key) = config.llm.kimi.api_key {
let client = crate::llm::KimiClient::with_base_url(
key,
&config.llm.kimi.model,
&config.llm.kimi.base_url,
)?;
println!("Fetching available models from Kimi...");
match client.list_models().await {
Ok(models) => {
println!("\n{}", "Available models:".bold());
for model in models {
let marker = if model == config.llm.kimi.model { "".green() } else { "".dimmed() };
println!("{} {}", marker, model);
}
}
Err(e) => {
println!("{} Failed to fetch models: {}", "".red(), e);
}
}
} else {
bail!("Kimi API key not configured");
}
}
"deepseek" => {
if let Some(ref key) = config.llm.deepseek.api_key {
let client = crate::llm::DeepSeekClient::with_base_url(
key,
&config.llm.deepseek.model,
&config.llm.deepseek.base_url,
)?;
println!("Fetching available models from DeepSeek...");
match client.list_models().await {
Ok(models) => {
println!("\n{}", "Available models:".bold());
for model in models {
let marker = if model == config.llm.deepseek.model { "".green() } else { "".dimmed() };
println!("{} {}", marker, model);
}
}
Err(e) => {
println!("{} Failed to fetch models: {}", "".red(), e);
}
}
} else {
bail!("DeepSeek API key not configured");
}
}
"openrouter" => {
if let Some(ref key) = config.llm.openrouter.api_key {
let client = crate::llm::OpenRouterClient::with_base_url(
key,
&config.llm.openrouter.model,
&config.llm.openrouter.base_url,
)?;
println!("Fetching available models from OpenRouter...");
match client.list_models().await {
Ok(models) => {
println!("\n{}", "Available models:".bold());
for model in models {
let marker = if model == config.llm.openrouter.model { "".green() } else { "".dimmed() };
println!("{} {}", marker, model);
}
}
Err(e) => {
println!("{} Failed to fetch models: {}", "".red(), e);
}
}
} else {
bail!("OpenRouter API key not configured");
}
}
provider => { provider => {
println!("Listing models not supported for provider: {}", provider); println!("Listing models not supported for provider: {}", provider);
} }

View File

@@ -22,15 +22,41 @@ impl GitRepo {
/// Open a git repository /// Open a git repository
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> { pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref(); let path = path.as_ref();
let absolute_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
// Enhanced cross-platform path handling
let absolute_path = if let Ok(canonical) = path.canonicalize() {
canonical
} else {
// Fallback: convert to absolute path without canonicalization
if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
}
};
// Try multiple git repository discovery strategies for cross-platform compatibility
let repo = Repository::discover(&absolute_path) let repo = Repository::discover(&absolute_path)
.or_else(|_| Repository::open(&absolute_path)) .or_else(|discover_err| {
// Try direct open as fallback
Repository::open(&absolute_path).map_err(|open_err| {
// Provide detailed error information for debugging
anyhow::anyhow!(
"Git repository discovery failed:\n\
Discovery error: {}\n\
Direct open error: {}\n\
Path attempted: {:?}\n\
Current directory: {:?}",
discover_err, open_err, absolute_path, std::env::current_dir()
)
})
})
.with_context(|| { .with_context(|| {
format!( format!(
"Failed to open git repository at '{:?}'. Please ensure:\n\ "Failed to open git repository at '{:?}'. Please ensure:\n\
1. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\ 1. The directory contains a valid '.git' folder\n\
2. The path is correct and contains a valid '.git' folder.", 2. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\
3. You have proper permissions to access the repository",
absolute_path, absolute_path,
absolute_path.display() absolute_path.display()
) )
@@ -689,19 +715,37 @@ impl StatusSummary {
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> { pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
let start_path = start_path.as_ref(); let start_path = start_path.as_ref();
// Try the starting path first
if let Ok(repo) = GitRepo::open(start_path) { if let Ok(repo) = GitRepo::open(start_path) {
return Ok(repo); return Ok(repo);
} }
// Walk up the directory tree to find a git repository
let mut current = start_path; let mut current = start_path;
let mut attempted_paths = vec![current.to_string_lossy().to_string()];
while let Some(parent) = current.parent() { while let Some(parent) = current.parent() {
attempted_paths.push(parent.to_string_lossy().to_string());
if let Ok(repo) = GitRepo::open(parent) { if let Ok(repo) = GitRepo::open(parent) {
return Ok(repo); return Ok(repo);
} }
current = parent; current = parent;
} }
bail!("No git repository found starting from {:?}", start_path) // Provide detailed error information for debugging
bail!(
"No git repository found starting from {:?}.\n\
Paths attempted:\n {}\n\
Current directory: {:?}\n\
Please ensure:\n\
1. You are in a git repository or its subdirectory\n\
2. The repository has a valid .git folder\n\
3. You have proper permissions to access the repository",
start_path,
attempted_paths.join("\n "),
std::env::current_dir()
)
} }
/// Check if path is inside a git repository /// Check if path is inside a git repository

View File

@@ -70,6 +70,12 @@ impl AnthropicClient {
Ok(self) Ok(self)
} }
/// List available models
pub async fn list_models(&self) -> Result<Vec<String>> {
// Anthropic doesn't have a models API endpoint, return predefined list
Ok(ANTHROPIC_MODELS.iter().map(|&m| m.to_string()).collect())
}
/// Validate API key /// Validate API key
pub async fn validate_key(&self) -> Result<bool> { pub async fn validate_key(&self) -> Result<bool> {
let url = "https://api.anthropic.com/v1/messages"; let url = "https://api.anthropic.com/v1/messages";

View File

@@ -82,8 +82,8 @@ impl DeepSeekClient {
Ok(self) Ok(self)
} }
/// Validate API key /// List available models
pub async fn validate_key(&self) -> Result<bool> { pub async fn list_models(&self) -> Result<Vec<String>> {
let url = format!("{}/models", self.base_url); let url = format!("{}/models", self.base_url);
let response = self.client let response = self.client
@@ -91,16 +91,44 @@ impl DeepSeekClient {
.header("Authorization", format!("Bearer {}", self.api_key)) .header("Authorization", format!("Bearer {}", self.api_key))
.send() .send()
.await .await
.context("Failed to validate DeepSeek API key")?; .context("Failed to list DeepSeek models")?;
if response.status().is_success() { if !response.status().is_success() {
Ok(true)
} else if response.status().as_u16() == 401 {
Ok(false)
} else {
let status = response.status(); let status = response.status();
let text = response.text().await.unwrap_or_default(); let text = response.text().await.unwrap_or_default();
bail!("DeepSeek API error: {} - {}", status, text) bail!("DeepSeek API error: {} - {}", status, text);
}
#[derive(Deserialize)]
struct ModelsResponse {
data: Vec<Model>,
}
#[derive(Deserialize)]
struct Model {
id: String,
}
let result: ModelsResponse = response
.json()
.await
.context("Failed to parse DeepSeek response")?;
Ok(result.data.into_iter().map(|m| m.id).collect())
}
/// Validate API key
pub async fn validate_key(&self) -> Result<bool> {
match self.list_models().await {
Ok(_) => Ok(true),
Err(e) => {
let err_str = e.to_string();
if err_str.contains("401") || err_str.contains("Unauthorized") {
Ok(false)
} else {
Err(e)
}
}
} }
} }
} }

View File

@@ -82,8 +82,8 @@ impl KimiClient {
Ok(self) Ok(self)
} }
/// Validate API key /// List available models
pub async fn validate_key(&self) -> Result<bool> { pub async fn list_models(&self) -> Result<Vec<String>> {
let url = format!("{}/models", self.base_url); let url = format!("{}/models", self.base_url);
let response = self.client let response = self.client
@@ -91,16 +91,44 @@ impl KimiClient {
.header("Authorization", format!("Bearer {}", self.api_key)) .header("Authorization", format!("Bearer {}", self.api_key))
.send() .send()
.await .await
.context("Failed to validate Kimi API key")?; .context("Failed to list Kimi models")?;
if response.status().is_success() { if !response.status().is_success() {
Ok(true)
} else if response.status().as_u16() == 401 {
Ok(false)
} else {
let status = response.status(); let status = response.status();
let text = response.text().await.unwrap_or_default(); let text = response.text().await.unwrap_or_default();
bail!("Kimi API error: {} - {}", status, text) bail!("Kimi API error: {} - {}", status, text);
}
#[derive(Deserialize)]
struct ModelsResponse {
data: Vec<Model>,
}
#[derive(Deserialize)]
struct Model {
id: String,
}
let result: ModelsResponse = response
.json()
.await
.context("Failed to parse Kimi response")?;
Ok(result.data.into_iter().map(|m| m.id).collect())
}
/// Validate API key
pub async fn validate_key(&self) -> Result<bool> {
match self.list_models().await {
Ok(_) => Ok(true),
Err(e) => {
let err_str = e.to_string();
if err_str.contains("401") || err_str.contains("Unauthorized") {
Ok(false)
} else {
Err(e)
}
}
} }
} }
} }

View File

@@ -82,8 +82,8 @@ impl OpenRouterClient {
Ok(self) Ok(self)
} }
/// Validate API key /// List available models
pub async fn validate_key(&self) -> Result<bool> { pub async fn list_models(&self) -> Result<Vec<String>> {
let url = format!("{}/models", self.base_url); let url = format!("{}/models", self.base_url);
let response = self.client let response = self.client
@@ -93,16 +93,44 @@ impl OpenRouterClient {
.header("X-Title", "QuiCommit") .header("X-Title", "QuiCommit")
.send() .send()
.await .await
.context("Failed to validate OpenRouter API key")?; .context("Failed to list OpenRouter models")?;
if response.status().is_success() { if !response.status().is_success() {
Ok(true)
} else if response.status().as_u16() == 401 {
Ok(false)
} else {
let status = response.status(); let status = response.status();
let text = response.text().await.unwrap_or_default(); let text = response.text().await.unwrap_or_default();
bail!("OpenRouter API error: {} - {}", status, text) bail!("OpenRouter API error: {} - {}", status, text);
}
#[derive(Deserialize)]
struct ModelsResponse {
data: Vec<Model>,
}
#[derive(Deserialize)]
struct Model {
id: String,
}
let result: ModelsResponse = response
.json()
.await
.context("Failed to parse OpenRouter response")?;
Ok(result.data.into_iter().map(|m| m.id).collect())
}
/// Validate API key
pub async fn validate_key(&self) -> Result<bool> {
match self.list_models().await {
Ok(_) => Ok(true),
Err(e) => {
let err_str = e.to_string();
if err_str.contains("401") || err_str.contains("Unauthorized") {
Ok(false)
} else {
Err(e)
}
}
} }
} }
} }