设置界面支持版本控制和github账号管理
This commit is contained in:
@@ -5,4 +5,5 @@ pub mod folders;
|
||||
pub mod mcp;
|
||||
pub mod system_settings;
|
||||
pub mod terminal;
|
||||
pub mod version_control;
|
||||
pub mod windows;
|
||||
|
||||
288
src-tauri/src/commands/version_control.rs
Normal file
288
src-tauri/src/commands/version_control.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::app_error::AppCommandError;
|
||||
use crate::db::service::app_metadata_service;
|
||||
use crate::db::AppDatabase;
|
||||
use crate::models::{
|
||||
GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings,
|
||||
};
|
||||
|
||||
const GIT_SETTINGS_KEY: &str = "git_settings";
|
||||
const GITHUB_ACCOUNTS_KEY: &str = "github_accounts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn run_git_version(git_path: &str) -> Result<GitDetectResult, AppCommandError> {
|
||||
let output = crate::process::tokio_command(git_path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
AppCommandError::not_found(format!("Cannot execute git at: {git_path}"))
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(GitDetectResult {
|
||||
installed: false,
|
||||
version: None,
|
||||
path: Some(git_path.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let version = stdout
|
||||
.trim()
|
||||
.strip_prefix("git version ")
|
||||
.unwrap_or(stdout.trim())
|
||||
.to_string();
|
||||
|
||||
Ok(GitDetectResult {
|
||||
installed: true,
|
||||
version: Some(version),
|
||||
path: Some(git_path.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn detect_git_path() -> Option<String> {
|
||||
let which_cmd = if cfg!(target_os = "windows") {
|
||||
"where"
|
||||
} else {
|
||||
"which"
|
||||
};
|
||||
|
||||
let output = crate::process::tokio_command(which_cmd)
|
||||
.arg("git")
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.next()?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn detect_git(
|
||||
db: State<'_, AppDatabase>,
|
||||
) -> Result<GitDetectResult, AppCommandError> {
|
||||
// Check if there's a custom path configured
|
||||
let settings = load_git_settings(&db.conn).await?;
|
||||
|
||||
if let Some(custom) = &settings.custom_path {
|
||||
let trimmed = custom.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return run_git_version(trimmed).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect
|
||||
if let Some(path) = detect_git_path().await {
|
||||
return run_git_version(&path).await;
|
||||
}
|
||||
|
||||
// Fallback: try "git" directly (might be in PATH but `which` failed)
|
||||
match run_git_version("git").await {
|
||||
Ok(result) if result.installed => Ok(result),
|
||||
_ => Ok(GitDetectResult {
|
||||
installed: false,
|
||||
version: None,
|
||||
path: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn test_git_path(path: String) -> Result<GitDetectResult, AppCommandError> {
|
||||
let trimmed = path.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppCommandError::invalid_input("Git path cannot be empty"));
|
||||
}
|
||||
run_git_version(trimmed).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Git settings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn load_git_settings(
|
||||
conn: &sea_orm::DatabaseConnection,
|
||||
) -> Result<GitSettings, AppCommandError> {
|
||||
let raw = app_metadata_service::get_value(conn, GIT_SETTINGS_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
match raw {
|
||||
Some(raw) => serde_json::from_str::<GitSettings>(&raw).map_err(|e| {
|
||||
AppCommandError::configuration_invalid("Failed to parse stored git settings")
|
||||
.with_detail(e.to_string())
|
||||
}),
|
||||
None => Ok(GitSettings::default()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_git_settings(
|
||||
db: State<'_, AppDatabase>,
|
||||
) -> Result<GitSettings, AppCommandError> {
|
||||
load_git_settings(&db.conn).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_git_settings(
|
||||
settings: GitSettings,
|
||||
db: State<'_, AppDatabase>,
|
||||
) -> Result<GitSettings, AppCommandError> {
|
||||
let serialized = serde_json::to_string(&settings).map_err(|e| {
|
||||
AppCommandError::invalid_input("Failed to serialize git settings")
|
||||
.with_detail(e.to_string())
|
||||
})?;
|
||||
|
||||
app_metadata_service::upsert_value(&db.conn, GIT_SETTINGS_KEY, &serialized)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GitHub accounts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn load_github_accounts(
|
||||
conn: &sea_orm::DatabaseConnection,
|
||||
) -> Result<GitHubAccountsSettings, AppCommandError> {
|
||||
let raw = app_metadata_service::get_value(conn, GITHUB_ACCOUNTS_KEY)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
match raw {
|
||||
Some(raw) => serde_json::from_str::<GitHubAccountsSettings>(&raw).map_err(|e| {
|
||||
AppCommandError::configuration_invalid("Failed to parse stored GitHub accounts")
|
||||
.with_detail(e.to_string())
|
||||
}),
|
||||
None => Ok(GitHubAccountsSettings::default()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_github_accounts(
|
||||
db: State<'_, AppDatabase>,
|
||||
) -> Result<GitHubAccountsSettings, AppCommandError> {
|
||||
load_github_accounts(&db.conn).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_github_accounts(
|
||||
settings: GitHubAccountsSettings,
|
||||
db: State<'_, AppDatabase>,
|
||||
) -> Result<GitHubAccountsSettings, AppCommandError> {
|
||||
let serialized = serde_json::to_string(&settings).map_err(|e| {
|
||||
AppCommandError::invalid_input("Failed to serialize GitHub accounts")
|
||||
.with_detail(e.to_string())
|
||||
})?;
|
||||
|
||||
app_metadata_service::upsert_value(&db.conn, GITHUB_ACCOUNTS_KEY, &serialized)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GitHubUserResponse {
|
||||
login: String,
|
||||
avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn validate_github_token(
|
||||
server_url: String,
|
||||
token: String,
|
||||
) -> Result<GitHubTokenValidation, AppCommandError> {
|
||||
let trimmed_url = server_url.trim().trim_end_matches('/');
|
||||
let trimmed_token = token.trim();
|
||||
|
||||
if trimmed_token.is_empty() {
|
||||
return Err(AppCommandError::invalid_input("Token cannot be empty"));
|
||||
}
|
||||
|
||||
// Build API URL: github.com uses api.github.com, enterprise uses {url}/api/v3
|
||||
let api_url = if trimmed_url.is_empty()
|
||||
|| trimmed_url == "https://github.com"
|
||||
|| trimmed_url == "http://github.com"
|
||||
{
|
||||
"https://api.github.com/user".to_string()
|
||||
} else {
|
||||
format!("{trimmed_url}/api/v3/user")
|
||||
};
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(&api_url)
|
||||
.header("Authorization", format!("Bearer {trimmed_token}"))
|
||||
.header("User-Agent", "codeg")
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| AppCommandError::network("Failed to connect to GitHub API").with_detail(e.to_string()))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status().as_u16();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
let message = if status == 401 {
|
||||
"Invalid or expired token".to_string()
|
||||
} else {
|
||||
format!("GitHub API returned status {status}: {body}")
|
||||
};
|
||||
return Ok(GitHubTokenValidation {
|
||||
success: false,
|
||||
username: None,
|
||||
scopes: vec![],
|
||||
avatar_url: None,
|
||||
message: Some(message),
|
||||
});
|
||||
}
|
||||
|
||||
// Parse scopes from x-oauth-scopes header
|
||||
let scopes: Vec<String> = response
|
||||
.headers()
|
||||
.get("x-oauth-scopes")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| {
|
||||
s.split(',')
|
||||
.map(|scope| scope.trim().to_string())
|
||||
.filter(|scope| !scope.is_empty())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let user = response
|
||||
.json::<GitHubUserResponse>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppCommandError::network("Failed to parse GitHub API response")
|
||||
.with_detail(e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(GitHubTokenValidation {
|
||||
success: true,
|
||||
username: Some(user.login),
|
||||
scopes,
|
||||
avatar_url: user.avatar_url,
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use acp::manager::ConnectionManager;
|
||||
use commands::{
|
||||
acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands,
|
||||
system_settings, terminal as terminal_commands, windows,
|
||||
system_settings, terminal as terminal_commands, version_control, windows,
|
||||
};
|
||||
use tauri::Manager;
|
||||
use terminal::manager::TerminalManager;
|
||||
@@ -264,6 +264,13 @@ pub fn run() {
|
||||
system_settings::update_system_proxy_settings,
|
||||
system_settings::get_system_language_settings,
|
||||
system_settings::update_system_language_settings,
|
||||
version_control::detect_git,
|
||||
version_control::test_git_path,
|
||||
version_control::get_git_settings,
|
||||
version_control::update_git_settings,
|
||||
version_control::get_github_accounts,
|
||||
version_control::validate_github_token,
|
||||
version_control::update_github_accounts,
|
||||
acp_commands::acp_preflight,
|
||||
acp_commands::acp_connect,
|
||||
acp_commands::acp_prompt,
|
||||
|
||||
@@ -12,4 +12,7 @@ pub use conversation::{
|
||||
};
|
||||
pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedConversation};
|
||||
pub use message::{ContentBlock, MessageRole, MessageTurn, TurnRole, TurnUsage, UnifiedMessage};
|
||||
pub use system::{SystemLanguageSettings, SystemProxySettings};
|
||||
pub use system::{
|
||||
GitDetectResult, GitHubAccountsSettings, GitHubTokenValidation, GitSettings,
|
||||
SystemLanguageSettings, SystemProxySettings,
|
||||
};
|
||||
|
||||
@@ -36,3 +36,45 @@ pub struct SystemLanguageSettings {
|
||||
pub mode: LanguageMode,
|
||||
pub language: AppLocale,
|
||||
}
|
||||
|
||||
// --- Version Control ---
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitDetectResult {
|
||||
pub installed: bool,
|
||||
pub version: Option<String>,
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct GitSettings {
|
||||
pub custom_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitHubAccount {
|
||||
pub id: String,
|
||||
pub server_url: String,
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub is_default: bool,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct GitHubAccountsSettings {
|
||||
pub accounts: Vec<GitHubAccount>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GitHubTokenValidation {
|
||||
pub success: bool,
|
||||
pub username: Option<String>,
|
||||
pub scopes: Vec<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user