✨ 新增个人访问令牌、使用统计与配置校验功能
This commit is contained in:
@@ -4,9 +4,9 @@ use colored::Colorize;
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
|
||||
use crate::config::manager::ConfigManager;
|
||||
use crate::config::{GitProfile};
|
||||
use crate::config::{GitProfile, TokenConfig, TokenType, ProfileComparison};
|
||||
use crate::config::profile::{GpgConfig, SshConfig};
|
||||
use crate::git::find_repo;
|
||||
use crate::git::{find_repo, GitConfigHelper, UserConfig};
|
||||
use crate::utils::validators::validate_profile_name;
|
||||
|
||||
/// Manage Git profiles
|
||||
@@ -75,6 +75,51 @@ enum ProfileSubcommand {
|
||||
/// New profile name
|
||||
to: String,
|
||||
},
|
||||
|
||||
/// Manage tokens for a profile
|
||||
Token {
|
||||
#[command(subcommand)]
|
||||
token_command: TokenSubcommand,
|
||||
},
|
||||
|
||||
/// Check profile configuration against git
|
||||
Check {
|
||||
/// Profile name
|
||||
name: Option<String>,
|
||||
},
|
||||
|
||||
/// Show usage statistics
|
||||
Stats {
|
||||
/// Profile name
|
||||
name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum TokenSubcommand {
|
||||
/// Add a token to a profile
|
||||
Add {
|
||||
/// Profile name
|
||||
profile: String,
|
||||
|
||||
/// Service name (e.g., github, gitlab)
|
||||
service: String,
|
||||
},
|
||||
|
||||
/// Remove a token from a profile
|
||||
Remove {
|
||||
/// Profile name
|
||||
profile: String,
|
||||
|
||||
/// Service name
|
||||
service: String,
|
||||
},
|
||||
|
||||
/// List tokens in a profile
|
||||
List {
|
||||
/// Profile name
|
||||
profile: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProfileCommand {
|
||||
@@ -90,6 +135,9 @@ impl ProfileCommand {
|
||||
Some(ProfileSubcommand::Apply { name, global }) => self.apply_profile(name.as_deref(), *global).await,
|
||||
Some(ProfileSubcommand::Switch) => self.switch_profile().await,
|
||||
Some(ProfileSubcommand::Copy { from, to }) => self.copy_profile(from, to).await,
|
||||
Some(ProfileSubcommand::Token { token_command }) => self.handle_token_command(token_command).await,
|
||||
Some(ProfileSubcommand::Check { name }) => self.check_profile(name.as_deref()).await,
|
||||
Some(ProfileSubcommand::Stats { name }) => self.show_stats(name.as_deref()).await,
|
||||
None => self.list_profiles().await,
|
||||
}
|
||||
}
|
||||
@@ -148,7 +196,6 @@ impl ProfileCommand {
|
||||
profile.is_work = is_work;
|
||||
profile.organization = organization;
|
||||
|
||||
// SSH configuration
|
||||
let setup_ssh = Confirm::new()
|
||||
.with_prompt("Configure SSH key?")
|
||||
.default(false)
|
||||
@@ -158,7 +205,6 @@ impl ProfileCommand {
|
||||
profile.ssh = Some(self.setup_ssh_interactive().await?);
|
||||
}
|
||||
|
||||
// GPG configuration
|
||||
let setup_gpg = Confirm::new()
|
||||
.with_prompt("Configure GPG signing?")
|
||||
.default(false)
|
||||
@@ -168,12 +214,20 @@ impl ProfileCommand {
|
||||
profile.gpg = Some(self.setup_gpg_interactive().await?);
|
||||
}
|
||||
|
||||
let setup_token = Confirm::new()
|
||||
.with_prompt("Add a Personal Access Token?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if setup_token {
|
||||
self.setup_token_interactive(&mut profile).await?;
|
||||
}
|
||||
|
||||
manager.add_profile(name.clone(), profile)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Profile '{}' added successfully", "✓".green(), name.cyan());
|
||||
|
||||
// Offer to set as default
|
||||
if manager.default_profile().is_none() {
|
||||
let set_default = Confirm::new()
|
||||
.with_prompt("Set as default profile?")
|
||||
@@ -251,6 +305,13 @@ impl ProfileCommand {
|
||||
if profile.has_gpg() {
|
||||
println!(" {} GPG configured", "🔒".to_string().dimmed());
|
||||
}
|
||||
if profile.has_tokens() {
|
||||
println!(" {} {} token(s)", "🔐".to_string().dimmed(), profile.tokens.len());
|
||||
}
|
||||
|
||||
if let Some(ref usage) = profile.usage.last_used {
|
||||
println!(" {} Last used: {}", "📊".to_string().dimmed(), usage.dimmed());
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
@@ -298,6 +359,24 @@ impl ProfileCommand {
|
||||
println!(" Use agent: {}", if gpg.use_agent { "yes" } else { "no" });
|
||||
}
|
||||
|
||||
if !profile.tokens.is_empty() {
|
||||
println!("\n{}", "Tokens:".bold());
|
||||
for (service, token) in &profile.tokens {
|
||||
println!(" {} ({})", service.cyan(), token.token_type);
|
||||
if let Some(ref desc) = token.description {
|
||||
println!(" {}", desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if profile.usage.total_uses > 0 {
|
||||
println!("\n{}", "Usage Statistics:".bold());
|
||||
println!(" Total uses: {}", profile.usage.total_uses);
|
||||
if let Some(ref last_used) = profile.usage.last_used {
|
||||
println!(" Last used: {}", last_used);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -330,6 +409,8 @@ impl ProfileCommand {
|
||||
new_profile.organization = profile.organization;
|
||||
new_profile.ssh = profile.ssh;
|
||||
new_profile.gpg = profile.gpg;
|
||||
new_profile.tokens = profile.tokens;
|
||||
new_profile.usage = profile.usage;
|
||||
|
||||
manager.update_profile(name, new_profile)?;
|
||||
manager.save()?;
|
||||
@@ -365,18 +446,27 @@ impl ProfileCommand {
|
||||
}
|
||||
|
||||
async fn apply_profile(&self, name: Option<&str>, global: bool) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
let profile = if let Some(n) = name {
|
||||
manager.get_profile(n)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", n))?
|
||||
.clone()
|
||||
let profile_name = if let Some(n) = name {
|
||||
n.to_string()
|
||||
} else {
|
||||
manager.default_profile()
|
||||
manager.default_profile_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
let profile = manager.get_profile(&profile_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?
|
||||
.clone();
|
||||
|
||||
let repo_path = if global {
|
||||
None
|
||||
} else {
|
||||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||||
Some(repo.path().to_string_lossy().to_string())
|
||||
};
|
||||
|
||||
if global {
|
||||
profile.apply_global()?;
|
||||
println!("{} Applied profile '{}' globally", "✓".green(), profile.name.cyan());
|
||||
@@ -386,6 +476,9 @@ impl ProfileCommand {
|
||||
println!("{} Applied profile '{}' to current repository", "✓".green(), profile.name.cyan());
|
||||
}
|
||||
|
||||
manager.record_profile_usage(&profile_name, repo_path)?;
|
||||
manager.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -419,7 +512,6 @@ impl ProfileCommand {
|
||||
|
||||
println!("{} Switched to profile '{}'", "✓".green(), selected.cyan());
|
||||
|
||||
// Offer to apply to current repo
|
||||
if find_repo(".").is_ok() {
|
||||
let apply = Confirm::new()
|
||||
.with_prompt("Apply to current repository?")
|
||||
@@ -445,6 +537,7 @@ impl ProfileCommand {
|
||||
|
||||
let mut new_profile = source.clone();
|
||||
new_profile.name = to.to_string();
|
||||
new_profile.usage = Default::default();
|
||||
|
||||
manager.add_profile(to.to_string(), new_profile)?;
|
||||
manager.save()?;
|
||||
@@ -454,6 +547,192 @@ impl ProfileCommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_token_command(&self, cmd: &TokenSubcommand) -> Result<()> {
|
||||
match cmd {
|
||||
TokenSubcommand::Add { profile, service } => self.add_token(profile, service).await,
|
||||
TokenSubcommand::Remove { profile, service } => self.remove_token(profile, service).await,
|
||||
TokenSubcommand::List { profile } => self.list_tokens(profile).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_token(&self, profile_name: &str, service: &str) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
if !manager.has_profile(profile_name) {
|
||||
bail!("Profile '{}' not found", profile_name);
|
||||
}
|
||||
|
||||
println!("{}", format!("\nAdd token to profile '{}'", profile_name).bold());
|
||||
println!("{}", "─".repeat(40));
|
||||
|
||||
let token_value: String = Input::new()
|
||||
.with_prompt(&format!("Token for {}", service))
|
||||
.interact_text()?;
|
||||
|
||||
let token_type_options = vec!["Personal", "OAuth", "Deploy", "App"];
|
||||
let selection = Select::new()
|
||||
.with_prompt("Token type")
|
||||
.items(&token_type_options)
|
||||
.default(0)
|
||||
.interact()?;
|
||||
|
||||
let token_type = match selection {
|
||||
0 => TokenType::Personal,
|
||||
1 => TokenType::OAuth,
|
||||
2 => TokenType::Deploy,
|
||||
3 => TokenType::App,
|
||||
_ => TokenType::Personal,
|
||||
};
|
||||
|
||||
let description: String = Input::new()
|
||||
.with_prompt("Description (optional)")
|
||||
.allow_empty(true)
|
||||
.interact_text()?;
|
||||
|
||||
let mut token = TokenConfig::new(token_value, token_type);
|
||||
if !description.is_empty() {
|
||||
token.description = Some(description);
|
||||
}
|
||||
|
||||
manager.add_token_to_profile(profile_name, service.to_string(), token)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Token for '{}' added to profile '{}'", "✓".green(), service.cyan(), profile_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_token(&self, profile_name: &str, service: &str) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
if !manager.has_profile(profile_name) {
|
||||
bail!("Profile '{}' not found", profile_name);
|
||||
}
|
||||
|
||||
let confirm = Confirm::new()
|
||||
.with_prompt(&format!("Remove token '{}' from profile '{}'?", service, profile_name))
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if !confirm {
|
||||
println!("{}", "Cancelled.".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
manager.remove_token_from_profile(profile_name, service)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Token '{}' removed from profile '{}'", "✓".green(), service, profile_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_tokens(&self, profile_name: &str) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
|
||||
let profile = manager.get_profile(profile_name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
|
||||
|
||||
if profile.tokens.is_empty() {
|
||||
println!("{} No tokens configured for profile '{}'", "ℹ".yellow(), profile_name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", format!("\nTokens for profile '{}':", profile_name).bold());
|
||||
println!("{}", "─".repeat(40));
|
||||
|
||||
for (service, token) in &profile.tokens {
|
||||
println!("{} ({})", service.cyan().bold(), token.token_type);
|
||||
if let Some(ref desc) = token.description {
|
||||
println!(" {}", desc);
|
||||
}
|
||||
if let Some(ref last_used) = token.last_used {
|
||||
println!(" Last used: {}", last_used);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_profile(&self, name: Option<&str>) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
|
||||
let profile_name = if let Some(n) = name {
|
||||
n.to_string()
|
||||
} else {
|
||||
manager.default_profile_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||||
let comparison = manager.check_profile_config(&profile_name, repo.inner())?;
|
||||
|
||||
println!("{}", format!("\nChecking profile '{}' against git configuration", profile_name).bold());
|
||||
println!("{}", "─".repeat(60));
|
||||
|
||||
if comparison.matches {
|
||||
println!("{} Profile configuration matches git settings", "✓".green().bold());
|
||||
} else {
|
||||
println!("{} Profile configuration differs from git settings", "✗".red().bold());
|
||||
println!("\n{}", "Differences:".bold());
|
||||
|
||||
for diff in &comparison.differences {
|
||||
println!("\n {}:", diff.key.cyan());
|
||||
println!(" Profile: {}", diff.profile_value.green());
|
||||
println!(" Git: {}", diff.git_value.yellow());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_stats(&self, name: Option<&str>) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
|
||||
if let Some(n) = name {
|
||||
let profile = manager.get_profile(n)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", n))?;
|
||||
|
||||
self.show_single_profile_stats(profile);
|
||||
} else {
|
||||
let profiles = manager.list_profiles();
|
||||
|
||||
if profiles.is_empty() {
|
||||
println!("{}", "No profiles configured.".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("{}", "\nProfile Usage Statistics:".bold());
|
||||
println!("{}", "─".repeat(60));
|
||||
|
||||
for profile_name in profiles {
|
||||
if let Some(profile) = manager.get_profile(profile_name) {
|
||||
self.show_single_profile_stats(profile);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_single_profile_stats(&self, profile: &GitProfile) {
|
||||
println!("{}", format!("\n{}", profile.name).bold());
|
||||
println!(" Total uses: {}", profile.usage.total_uses);
|
||||
|
||||
if let Some(ref last_used) = profile.usage.last_used {
|
||||
println!(" Last used: {}", last_used);
|
||||
}
|
||||
|
||||
if !profile.usage.repo_usage.is_empty() {
|
||||
println!(" Repositories:");
|
||||
for (repo, count) in &profile.usage.repo_usage {
|
||||
println!(" {} ({} uses)", repo, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup_ssh_interactive(&self) -> Result<SshConfig> {
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -489,4 +768,19 @@ impl ProfileCommand {
|
||||
use_agent: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn setup_token_interactive(&self, profile: &mut GitProfile) -> Result<()> {
|
||||
let service: String = Input::new()
|
||||
.with_prompt("Service name (e.g., github, gitlab)")
|
||||
.interact_text()?;
|
||||
|
||||
let token_value: String = Input::new()
|
||||
.with_prompt("Token value")
|
||||
.interact_text()?;
|
||||
|
||||
let token = TokenConfig::new(token_value, TokenType::Personal);
|
||||
profile.add_token(service, token);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user