✨ 新增个人访问令牌、使用统计与配置校验功能
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use super::{AppConfig, GitProfile};
|
||||
use super::{AppConfig, GitProfile, TokenConfig, TokenType};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -75,12 +75,10 @@ impl ConfigManager {
|
||||
bail!("Profile '{}' does not exist", name);
|
||||
}
|
||||
|
||||
// Check if it's the default profile
|
||||
if self.config.default_profile.as_ref() == Some(&name.to_string()) {
|
||||
self.config.default_profile = None;
|
||||
}
|
||||
|
||||
// Remove from repo mappings
|
||||
self.config.repo_profiles.retain(|_, v| v != name);
|
||||
|
||||
self.config.profiles.remove(name);
|
||||
@@ -144,6 +142,56 @@ impl ConfigManager {
|
||||
self.config.default_profile.as_ref()
|
||||
}
|
||||
|
||||
/// Record profile usage
|
||||
pub fn record_profile_usage(&mut self, name: &str, repo_path: Option<String>) -> Result<()> {
|
||||
if let Some(profile) = self.config.profiles.get_mut(name) {
|
||||
profile.record_usage(repo_path);
|
||||
self.modified = true;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Profile '{}' does not exist", name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get profile usage statistics
|
||||
pub fn get_profile_usage(&self, name: &str) -> Option<&super::UsageStats> {
|
||||
self.config.profiles.get(name).map(|p| &p.usage)
|
||||
}
|
||||
|
||||
// Token management
|
||||
|
||||
/// Add a token to a profile
|
||||
pub fn add_token_to_profile(&mut self, profile_name: &str, service: String, token: TokenConfig) -> Result<()> {
|
||||
if let Some(profile) = self.config.profiles.get_mut(profile_name) {
|
||||
profile.add_token(service, token);
|
||||
self.modified = true;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Profile '{}' does not exist", profile_name);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a token from a profile
|
||||
pub fn get_token_from_profile(&self, profile_name: &str, service: &str) -> Option<&TokenConfig> {
|
||||
self.config.profiles.get(profile_name)?.get_token(service)
|
||||
}
|
||||
|
||||
/// Remove a token from a profile
|
||||
pub fn remove_token_from_profile(&mut self, profile_name: &str, service: &str) -> Result<()> {
|
||||
if let Some(profile) = self.config.profiles.get_mut(profile_name) {
|
||||
profile.remove_token(service);
|
||||
self.modified = true;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Profile '{}' does not exist", profile_name);
|
||||
}
|
||||
}
|
||||
|
||||
/// List all tokens in a profile
|
||||
pub fn list_profile_tokens(&self, profile_name: &str) -> Option<Vec<&String>> {
|
||||
self.config.profiles.get(profile_name).map(|p| p.tokens.keys().collect())
|
||||
}
|
||||
|
||||
// Repository profile management
|
||||
|
||||
/// Get profile for repository
|
||||
@@ -185,6 +233,13 @@ impl ConfigManager {
|
||||
self.default_profile()
|
||||
}
|
||||
|
||||
/// Check and compare profile with git configuration
|
||||
pub fn check_profile_config(&self, profile_name: &str, repo: &git2::Repository) -> Result<super::ProfileComparison> {
|
||||
let profile = self.get_profile(profile_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
|
||||
profile.compare_with_git_config(repo)
|
||||
}
|
||||
|
||||
// LLM configuration
|
||||
|
||||
/// Get LLM provider
|
||||
|
||||
@@ -8,7 +8,10 @@ pub mod manager;
|
||||
pub mod profile;
|
||||
|
||||
pub use manager::ConfigManager;
|
||||
pub use profile::{GitProfile, ProfileSettings};
|
||||
pub use profile::{
|
||||
GitProfile, ProfileSettings, SshConfig, GpgConfig, TokenConfig, TokenType,
|
||||
UsageStats, ProfileComparison, ConfigDifference
|
||||
};
|
||||
|
||||
/// Application configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::{bail, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Git profile containing user identity and authentication settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -40,6 +41,14 @@ pub struct GitProfile {
|
||||
/// Company/Organization name (for work profiles)
|
||||
#[serde(default)]
|
||||
pub organization: Option<String>,
|
||||
|
||||
/// Personal Access Tokens (PATs) for different services
|
||||
#[serde(default)]
|
||||
pub tokens: HashMap<String, TokenConfig>,
|
||||
|
||||
/// Usage statistics
|
||||
#[serde(default)]
|
||||
pub usage: UsageStats,
|
||||
}
|
||||
|
||||
impl GitProfile {
|
||||
@@ -56,6 +65,8 @@ impl GitProfile {
|
||||
description: None,
|
||||
is_work: false,
|
||||
organization: None,
|
||||
tokens: HashMap::new(),
|
||||
usage: UsageStats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +95,10 @@ impl GitProfile {
|
||||
gpg.validate()?;
|
||||
}
|
||||
|
||||
for token in self.tokens.values() {
|
||||
token.validate()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -97,6 +112,11 @@ impl GitProfile {
|
||||
self.gpg.is_some() || self.signing_key.is_some()
|
||||
}
|
||||
|
||||
/// Check if profile has any tokens configured
|
||||
pub fn has_tokens(&self) -> bool {
|
||||
!self.tokens.is_empty()
|
||||
}
|
||||
|
||||
/// Get signing key (from GPG config or direct)
|
||||
pub fn signing_key(&self) -> Option<&str> {
|
||||
self.signing_key
|
||||
@@ -105,15 +125,44 @@ impl GitProfile {
|
||||
.or_else(|| self.gpg.as_ref().map(|g| g.key_id.as_str()))
|
||||
}
|
||||
|
||||
/// Apply this profile to a git repository
|
||||
/// Add a token to the profile
|
||||
pub fn add_token(&mut self, service: String, token: TokenConfig) {
|
||||
self.tokens.insert(service, token);
|
||||
}
|
||||
|
||||
/// Get a token for a specific service
|
||||
pub fn get_token(&self, service: &str) -> Option<&TokenConfig> {
|
||||
self.tokens.get(service)
|
||||
}
|
||||
|
||||
/// Remove a token from the profile
|
||||
pub fn remove_token(&mut self, service: &str) {
|
||||
self.tokens.remove(service);
|
||||
}
|
||||
|
||||
/// Record usage of this profile
|
||||
pub fn record_usage(&mut self, repo_path: Option<String>) {
|
||||
self.usage.last_used = Some(chrono::Utc::now().to_rfc3339());
|
||||
self.usage.total_uses += 1;
|
||||
|
||||
if let Some(repo) = repo_path {
|
||||
let count = self.usage.repo_usage.entry(repo).or_insert(0);
|
||||
*count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get usage statistics
|
||||
pub fn usage_stats(&self) -> &UsageStats {
|
||||
&self.usage
|
||||
}
|
||||
|
||||
/// Apply this profile to a git repository (local config)
|
||||
pub fn apply_to_repo(&self, repo: &git2::Repository) -> Result<()> {
|
||||
let mut config = repo.config()?;
|
||||
|
||||
// Set user info
|
||||
config.set_str("user.name", &self.user_name)?;
|
||||
config.set_str("user.email", &self.user_email)?;
|
||||
|
||||
// Set signing key if available
|
||||
if let Some(key) = self.signing_key() {
|
||||
config.set_str("user.signingkey", key)?;
|
||||
|
||||
@@ -126,7 +175,6 @@ impl GitProfile {
|
||||
}
|
||||
}
|
||||
|
||||
// Set SSH if configured
|
||||
if let Some(ref ssh) = self.ssh {
|
||||
if let Some(ref key_path) = ssh.private_key_path {
|
||||
config.set_str("core.sshCommand",
|
||||
@@ -150,6 +198,52 @@ impl GitProfile {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compare with current git configuration
|
||||
pub fn compare_with_git_config(&self, repo: &git2::Repository) -> Result<ProfileComparison> {
|
||||
let config = repo.config()?;
|
||||
|
||||
let git_user_name = config.get_string("user.name").ok();
|
||||
let git_user_email = config.get_string("user.email").ok();
|
||||
let git_signing_key = config.get_string("user.signingkey").ok();
|
||||
|
||||
let mut comparison = ProfileComparison {
|
||||
profile_name: self.name.clone(),
|
||||
matches: true,
|
||||
differences: vec![],
|
||||
};
|
||||
|
||||
if git_user_name.as_deref() != Some(&self.user_name) {
|
||||
comparison.matches = false;
|
||||
comparison.differences.push(ConfigDifference {
|
||||
key: "user.name".to_string(),
|
||||
profile_value: self.user_name.clone(),
|
||||
git_value: git_user_name.unwrap_or_else(|| "<not set>".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if git_user_email.as_deref() != Some(&self.user_email) {
|
||||
comparison.matches = false;
|
||||
comparison.differences.push(ConfigDifference {
|
||||
key: "user.email".to_string(),
|
||||
profile_value: self.user_email.clone(),
|
||||
git_value: git_user_email.unwrap_or_else(|| "<not set>".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(profile_key) = self.signing_key() {
|
||||
if git_signing_key.as_deref() != Some(profile_key) {
|
||||
comparison.matches = false;
|
||||
comparison.differences.push(ConfigDifference {
|
||||
key: "user.signingkey".to_string(),
|
||||
profile_value: profile_key.to_string(),
|
||||
git_value: git_signing_key.unwrap_or_else(|| "<not set>".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(comparison)
|
||||
}
|
||||
}
|
||||
|
||||
/// Profile settings
|
||||
@@ -285,6 +379,122 @@ impl GpgConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Token configuration for services (GitHub, GitLab, etc.)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TokenConfig {
|
||||
/// Token value (encrypted)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token: Option<String>,
|
||||
|
||||
/// Token type (personal, oauth, etc.)
|
||||
#[serde(default)]
|
||||
pub token_type: TokenType,
|
||||
|
||||
/// Token scopes/permissions
|
||||
#[serde(default)]
|
||||
pub scopes: Vec<String>,
|
||||
|
||||
/// Expiration date (RFC3339)
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<String>,
|
||||
|
||||
/// Last used timestamp
|
||||
#[serde(default)]
|
||||
pub last_used: Option<String>,
|
||||
|
||||
/// Description
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl TokenConfig {
|
||||
/// Create a new token config
|
||||
pub fn new(token: String, token_type: TokenType) -> Self {
|
||||
Self {
|
||||
token: Some(token),
|
||||
token_type,
|
||||
scopes: vec![],
|
||||
expires_at: None,
|
||||
last_used: None,
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate token configuration
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.token.is_none() && self.token_type != TokenType::None {
|
||||
bail!("Token value is required for {:?}", self.token_type);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record token usage
|
||||
pub fn record_usage(&mut self) {
|
||||
self.last_used = Some(chrono::Utc::now().to_rfc3339());
|
||||
}
|
||||
}
|
||||
|
||||
/// Token type
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TokenType {
|
||||
None,
|
||||
Personal,
|
||||
OAuth,
|
||||
Deploy,
|
||||
App,
|
||||
}
|
||||
|
||||
impl Default for TokenType {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TokenType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TokenType::None => write!(f, "none"),
|
||||
TokenType::Personal => write!(f, "personal"),
|
||||
TokenType::OAuth => write!(f, "oauth"),
|
||||
TokenType::Deploy => write!(f, "deploy"),
|
||||
TokenType::App => write!(f, "app"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Usage statistics for a profile
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct UsageStats {
|
||||
/// Total number of times this profile has been used
|
||||
#[serde(default)]
|
||||
pub total_uses: u64,
|
||||
|
||||
/// Last used timestamp (RFC3339)
|
||||
#[serde(default)]
|
||||
pub last_used: Option<String>,
|
||||
|
||||
/// Repository-specific usage counts
|
||||
#[serde(default)]
|
||||
pub repo_usage: HashMap<String, u64>,
|
||||
}
|
||||
|
||||
/// Comparison result between profile and git configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProfileComparison {
|
||||
pub profile_name: String,
|
||||
pub matches: bool,
|
||||
pub differences: Vec<ConfigDifference>,
|
||||
}
|
||||
|
||||
/// Configuration difference
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConfigDifference {
|
||||
pub key: String,
|
||||
pub profile_value: String,
|
||||
pub git_value: String,
|
||||
}
|
||||
|
||||
fn default_gpg_program() -> String {
|
||||
"gpg".to_string()
|
||||
}
|
||||
@@ -306,6 +516,7 @@ pub struct GitProfileBuilder {
|
||||
description: Option<String>,
|
||||
is_work: bool,
|
||||
organization: Option<String>,
|
||||
tokens: HashMap<String, TokenConfig>,
|
||||
}
|
||||
|
||||
impl GitProfileBuilder {
|
||||
@@ -359,6 +570,11 @@ impl GitProfileBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn token(mut self, service: impl Into<String>, token: TokenConfig) -> Self {
|
||||
self.tokens.insert(service.into(), token);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<GitProfile> {
|
||||
let name = self.name.ok_or_else(|| anyhow::anyhow!("Name is required"))?;
|
||||
let user_name = self.user_name.ok_or_else(|| anyhow::anyhow!("User name is required"))?;
|
||||
@@ -375,6 +591,8 @@ impl GitProfileBuilder {
|
||||
description: self.description,
|
||||
is_work: self.is_work,
|
||||
organization: self.organization,
|
||||
tokens: self.tokens,
|
||||
usage: UsageStats::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -409,4 +627,10 @@ mod tests {
|
||||
|
||||
assert!(profile.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_config() {
|
||||
let token = TokenConfig::new("test-token".to_string(), TokenType::Personal);
|
||||
assert!(token.validate().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user