chore: 升级版本至 0.1.10 并更新密钥环与加密相关描述
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "quicommit"
|
name = "quicommit"
|
||||||
version = "0.1.9"
|
version = "0.1.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Sidney Zhang <zly@lyzhang.me>"]
|
authors = ["Sidney Zhang <zly@lyzhang.me>"]
|
||||||
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
|
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ A powerful AI-powered Git assistant for generating conventional commits, tags, a
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ A powerful AI-powered Git assistant for generating conventional commits, tags, a
|
|||||||
- **Profile Management**: Manage multiple Git identities with SSH keys and GPG signing support
|
- **Profile Management**: Manage multiple Git identities with SSH keys and GPG signing support
|
||||||
- **Smart Tagging**: Semantic version bumping with AI-generated release notes
|
- **Smart Tagging**: Semantic version bumping with AI-generated release notes
|
||||||
- **Changelog Generation**: Automatic changelog generation in Keep a Changelog format
|
- **Changelog Generation**: Automatic changelog generation in Keep a Changelog format
|
||||||
- **Security**: Encrypt sensitive data
|
- **Security**: Use system keyring to store API keys securely
|
||||||
- **Interactive UI**: Beautiful CLI with previews and confirmations
|
- **Interactive UI**: Beautiful CLI with previews and confirmations
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## 主要功能
|
## 主要功能
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
- **多配置管理**:为不同场景管理多个Git身份,支持SSH密钥和GPG签名配置
|
- **多配置管理**:为不同场景管理多个Git身份,支持SSH密钥和GPG签名配置
|
||||||
- **智能标签管理**:基于语义版本自动检测升级,AI生成标签信息
|
- **智能标签管理**:基于语义版本自动检测升级,AI生成标签信息
|
||||||
- **变更日志生成**:自动生成Keep a Changelog格式的变更日志
|
- **变更日志生成**:自动生成Keep a Changelog格式的变更日志
|
||||||
- **安全保护**:加密存储敏感数据
|
- **安全保护**:使用系统密钥环进行安全存储
|
||||||
- **交互式界面**:美观的CLI界面,支持预览和确认
|
- **交互式界面**:美观的CLI界面,支持预览和确认
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use dialoguer::{Confirm, Input, Select};
|
use dialoguer::{Confirm, Input, Select, Password};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::{Language, manager::ConfigManager};
|
use crate::config::{Language, manager::ConfigManager};
|
||||||
use crate::config::CommitFormat;
|
use crate::config::CommitFormat;
|
||||||
use crate::utils::keyring::{get_supported_providers, get_default_model, get_default_base_url, provider_needs_api_key};
|
use crate::utils::keyring::{get_supported_providers, get_default_model, get_default_base_url, provider_needs_api_key};
|
||||||
|
use crate::utils::crypto::{encrypt, decrypt};
|
||||||
|
|
||||||
/// Mask API key with asterisks for security
|
/// Mask API key with asterisks for security
|
||||||
fn mask_api_key(key: Option<&str>) -> String {
|
fn mask_api_key(key: Option<&str>) -> String {
|
||||||
@@ -130,6 +131,10 @@ enum ConfigSubcommand {
|
|||||||
/// Output file (defaults to stdout)
|
/// Output file (defaults to stdout)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
output: Option<String>,
|
output: Option<String>,
|
||||||
|
|
||||||
|
/// Password for encryption (will prompt if not provided)
|
||||||
|
#[arg(short = 'p', long)]
|
||||||
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Import configuration
|
/// Import configuration
|
||||||
@@ -137,6 +142,10 @@ enum ConfigSubcommand {
|
|||||||
/// Input file
|
/// Input file
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
file: String,
|
file: String,
|
||||||
|
|
||||||
|
/// Password for decryption (will prompt if file is encrypted)
|
||||||
|
#[arg(short = 'p', long)]
|
||||||
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// List available LLM models
|
/// List available LLM models
|
||||||
@@ -172,8 +181,8 @@ impl ConfigCommand {
|
|||||||
Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => self.set_keep_types_english(*keep, &config_path).await,
|
Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => self.set_keep_types_english(*keep, &config_path).await,
|
||||||
Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => self.set_keep_changelog_types_english(*keep, &config_path).await,
|
Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => self.set_keep_changelog_types_english(*keep, &config_path).await,
|
||||||
Some(ConfigSubcommand::Reset { force }) => self.reset(*force, &config_path).await,
|
Some(ConfigSubcommand::Reset { force }) => self.reset(*force, &config_path).await,
|
||||||
Some(ConfigSubcommand::Export { output }) => self.export_config(output.as_deref(), &config_path).await,
|
Some(ConfigSubcommand::Export { output, password }) => self.export_config(output.as_deref(), password.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::Import { file }) => self.import_config(file, &config_path).await,
|
Some(ConfigSubcommand::Import { file, password }) => self.import_config(file, password.as_deref(), &config_path).await,
|
||||||
Some(ConfigSubcommand::ListModels) => self.list_models(&config_path).await,
|
Some(ConfigSubcommand::ListModels) => self.list_models(&config_path).await,
|
||||||
Some(ConfigSubcommand::TestLlm) => self.test_llm(&config_path).await,
|
Some(ConfigSubcommand::TestLlm) => self.test_llm(&config_path).await,
|
||||||
Some(ConfigSubcommand::Path) => self.show_path(&config_path).await,
|
Some(ConfigSubcommand::Path) => self.show_path(&config_path).await,
|
||||||
@@ -737,28 +746,90 @@ impl ConfigCommand {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn export_config(&self, output: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
async fn export_config(&self, output: Option<&str>, password: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let manager = self.get_manager(config_path)?;
|
let manager = self.get_manager(config_path)?;
|
||||||
let toml = manager.export()?;
|
let toml = manager.export()?;
|
||||||
|
|
||||||
|
let export_content = if let Some(path) = output {
|
||||||
|
let pwd = if let Some(p) = password {
|
||||||
|
p.to_string()
|
||||||
|
} else {
|
||||||
|
let confirm = Confirm::new()
|
||||||
|
.with_prompt("Encrypt the exported configuration?")
|
||||||
|
.default(true)
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
if confirm {
|
||||||
|
let pwd1 = Password::new()
|
||||||
|
.with_prompt("Enter encryption password")
|
||||||
|
.interact()?;
|
||||||
|
let pwd2 = Password::new()
|
||||||
|
.with_prompt("Confirm encryption password")
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
if pwd1 != pwd2 {
|
||||||
|
bail!("Passwords do not match");
|
||||||
|
}
|
||||||
|
pwd1
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if pwd.is_empty() {
|
||||||
|
toml
|
||||||
|
} else {
|
||||||
|
let encrypted = encrypt(toml.as_bytes(), &pwd)?;
|
||||||
|
format!("ENCRYPTED:{}", encrypted)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toml
|
||||||
|
};
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
Some(path) => {
|
Some(path) => {
|
||||||
std::fs::write(path, &toml)?;
|
std::fs::write(path, &export_content)?;
|
||||||
|
if export_content.starts_with("ENCRYPTED:") {
|
||||||
|
println!("{} Configuration encrypted and exported to {}", "✓".green(), path);
|
||||||
|
} else {
|
||||||
println!("{} Configuration exported to {}", "✓".green(), path);
|
println!("{} Configuration exported to {}", "✓".green(), path);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
println!("{}", toml);
|
println!("{}", export_content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn import_config(&self, file: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
async fn import_config(&self, file: &str, password: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||||||
let content = std::fs::read_to_string(file)?;
|
let content = std::fs::read_to_string(file)?;
|
||||||
|
|
||||||
|
let config_content = if content.starts_with("ENCRYPTED:") {
|
||||||
|
let encrypted_data = content.strip_prefix("ENCRYPTED:").unwrap();
|
||||||
|
|
||||||
|
let pwd = if let Some(p) = password {
|
||||||
|
p.to_string()
|
||||||
|
} else {
|
||||||
|
Password::new()
|
||||||
|
.with_prompt("Enter decryption password")
|
||||||
|
.interact()?
|
||||||
|
};
|
||||||
|
|
||||||
|
match decrypt(encrypted_data, &pwd) {
|
||||||
|
Ok(decrypted) => String::from_utf8(decrypted)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in decrypted content: {}", e))?,
|
||||||
|
Err(e) => {
|
||||||
|
bail!("Failed to decrypt configuration: {}. Please check your password.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
};
|
||||||
|
|
||||||
let mut manager = self.get_manager(config_path)?;
|
let mut manager = self.get_manager(config_path)?;
|
||||||
manager.import(&content)?;
|
manager.import(&config_content)?;
|
||||||
manager.save()?;
|
manager.save()?;
|
||||||
|
|
||||||
println!("{} Configuration imported from {}", "✓".green(), file);
|
println!("{} Configuration imported from {}", "✓".green(), file);
|
||||||
|
|||||||
359
tests/config_export_import_tests.rs
Normal file
359
tests/config_export_import_tests.rs
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
use assert_cmd::Command;
|
||||||
|
use predicates::prelude::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
fn create_git_repo(dir: &PathBuf) -> std::process::Output {
|
||||||
|
std::process::Command::new("git")
|
||||||
|
.args(&["init"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to init git repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_git_user(dir: &PathBuf) {
|
||||||
|
std::process::Command::new("git")
|
||||||
|
.args(&["config", "user.name", "Test User"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to configure git user name");
|
||||||
|
|
||||||
|
std::process::Command::new("git")
|
||||||
|
.args(&["config", "user.email", "test@example.com"])
|
||||||
|
.current_dir(dir)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to configure git user email");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_git_repo(dir: &PathBuf) {
|
||||||
|
create_git_repo(dir);
|
||||||
|
configure_git_user(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_quicommit(config_path: &PathBuf) {
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert().success();
|
||||||
|
}
|
||||||
|
|
||||||
|
mod config_export {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_export_to_stdout() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
init_quicommit(&config_path);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "export", "--config", config_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("version"))
|
||||||
|
.stdout(predicate::str::contains("[llm]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_export_to_file() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
let export_path = temp_dir.path().join("exported.toml");
|
||||||
|
init_quicommit(&config_path);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "export",
|
||||||
|
"--config", config_path.to_str().unwrap(),
|
||||||
|
"--output", export_path.to_str().unwrap(),
|
||||||
|
"--password", ""
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Configuration exported"));
|
||||||
|
|
||||||
|
assert!(export_path.exists(), "Export file should be created");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&export_path).unwrap();
|
||||||
|
assert!(content.contains("version"), "Export should contain version");
|
||||||
|
assert!(content.contains("[llm]"), "Export should contain LLM config");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_export_encrypted() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
let export_path = temp_dir.path().join("encrypted.toml");
|
||||||
|
init_quicommit(&config_path);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "export",
|
||||||
|
"--config", config_path.to_str().unwrap(),
|
||||||
|
"--output", export_path.to_str().unwrap(),
|
||||||
|
"--password", "test_password_123"
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("encrypted and exported"));
|
||||||
|
|
||||||
|
assert!(export_path.exists(), "Export file should be created");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&export_path).unwrap();
|
||||||
|
assert!(content.starts_with("ENCRYPTED:"), "Encrypted file should start with ENCRYPTED:");
|
||||||
|
assert!(!content.contains("[llm]"), "Encrypted content should not be readable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod config_import {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_import_plain_config() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
let import_path = temp_dir.path().join("import.toml");
|
||||||
|
|
||||||
|
let plain_config = r#"
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[llm]
|
||||||
|
provider = "openai"
|
||||||
|
model = "gpt-4"
|
||||||
|
max_tokens = 1000
|
||||||
|
temperature = 0.7
|
||||||
|
timeout = 60
|
||||||
|
api_key_storage = "keyring"
|
||||||
|
|
||||||
|
[commit]
|
||||||
|
format = "conventional"
|
||||||
|
auto_generate = true
|
||||||
|
allow_empty = false
|
||||||
|
gpg_sign = false
|
||||||
|
max_subject_length = 100
|
||||||
|
require_scope = false
|
||||||
|
require_body = false
|
||||||
|
body_required_types = ["feat", "fix"]
|
||||||
|
|
||||||
|
[tag]
|
||||||
|
version_prefix = "v"
|
||||||
|
auto_generate = true
|
||||||
|
gpg_sign = false
|
||||||
|
include_changelog = true
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
path = "CHANGELOG.md"
|
||||||
|
auto_generate = true
|
||||||
|
format = "keep-a-changelog"
|
||||||
|
include_hashes = false
|
||||||
|
include_authors = false
|
||||||
|
group_by_type = true
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
colors = true
|
||||||
|
icons = true
|
||||||
|
date_format = "%Y-%m-%d"
|
||||||
|
|
||||||
|
[language]
|
||||||
|
output_language = "en"
|
||||||
|
keep_types_english = true
|
||||||
|
keep_changelog_types_english = true
|
||||||
|
"#;
|
||||||
|
fs::write(&import_path, plain_config).unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "import",
|
||||||
|
"--config", config_path.to_str().unwrap(),
|
||||||
|
"--file", import_path.to_str().unwrap()
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Configuration imported"));
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "get", "llm.provider", "--config", config_path.to_str().unwrap()]);
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("openai"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_import_encrypted_config() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path1 = temp_dir.path().join("config1.toml");
|
||||||
|
let config_path2 = temp_dir.path().join("config2.toml");
|
||||||
|
let export_path = temp_dir.path().join("encrypted.toml");
|
||||||
|
|
||||||
|
init_quicommit(&config_path1);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "set", "llm.provider", "anthropic",
|
||||||
|
"--config", config_path1.to_str().unwrap()
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "export",
|
||||||
|
"--config", config_path1.to_str().unwrap(),
|
||||||
|
"--output", export_path.to_str().unwrap(),
|
||||||
|
"--password", "secure_password"
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "import",
|
||||||
|
"--config", config_path2.to_str().unwrap(),
|
||||||
|
"--file", export_path.to_str().unwrap(),
|
||||||
|
"--password", "secure_password"
|
||||||
|
]);
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Configuration imported"));
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "get", "llm.provider", "--config", config_path2.to_str().unwrap()]);
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("anthropic"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_import_encrypted_wrong_password() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
|
let export_path = temp_dir.path().join("encrypted.toml");
|
||||||
|
|
||||||
|
init_quicommit(&config_path);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "export",
|
||||||
|
"--config", config_path.to_str().unwrap(),
|
||||||
|
"--output", export_path.to_str().unwrap(),
|
||||||
|
"--password", "correct_password"
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "import",
|
||||||
|
"--config", config_path.to_str().unwrap(),
|
||||||
|
"--file", export_path.to_str().unwrap(),
|
||||||
|
"--password", "wrong_password"
|
||||||
|
]);
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("Failed to decrypt"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod config_export_import_roundtrip {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_roundtrip_plain() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path1 = temp_dir.path().join("config1.toml");
|
||||||
|
let config_path2 = temp_dir.path().join("config2.toml");
|
||||||
|
let export_path = temp_dir.path().join("export.toml");
|
||||||
|
|
||||||
|
init_quicommit(&config_path1);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "set", "llm.model", "gpt-4-turbo",
|
||||||
|
"--config", config_path1.to_str().unwrap()
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "export",
|
||||||
|
"--config", config_path1.to_str().unwrap(),
|
||||||
|
"--output", export_path.to_str().unwrap(),
|
||||||
|
"--password", ""
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "import",
|
||||||
|
"--config", config_path2.to_str().unwrap(),
|
||||||
|
"--file", export_path.to_str().unwrap()
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "get", "llm.model", "--config", config_path2.to_str().unwrap()]);
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("gpt-4-turbo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_roundtrip_encrypted() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path1 = temp_dir.path().join("config1.toml");
|
||||||
|
let config_path2 = temp_dir.path().join("config2.toml");
|
||||||
|
let export_path = temp_dir.path().join("encrypted.toml");
|
||||||
|
let password = "my_secure_password_123";
|
||||||
|
|
||||||
|
init_quicommit(&config_path1);
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "set", "llm.provider", "deepseek",
|
||||||
|
"--config", config_path1.to_str().unwrap()
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "set", "llm.model", "deepseek-chat",
|
||||||
|
"--config", config_path1.to_str().unwrap()
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "export",
|
||||||
|
"--config", config_path1.to_str().unwrap(),
|
||||||
|
"--output", export_path.to_str().unwrap(),
|
||||||
|
"--password", password
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let exported_content = fs::read_to_string(&export_path).unwrap();
|
||||||
|
assert!(exported_content.starts_with("ENCRYPTED:"));
|
||||||
|
assert!(!exported_content.contains("deepseek"));
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&[
|
||||||
|
"config", "import",
|
||||||
|
"--config", config_path2.to_str().unwrap(),
|
||||||
|
"--file", export_path.to_str().unwrap(),
|
||||||
|
"--password", password
|
||||||
|
]);
|
||||||
|
cmd.assert().success();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "get", "llm.provider", "--config", config_path2.to_str().unwrap()]);
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("deepseek"));
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("quicommit").unwrap();
|
||||||
|
cmd.args(&["config", "get", "llm.model", "--config", config_path2.to_str().unwrap()]);
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("deepseek-chat"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user