feat(git): add token authentication and credential discovery

Add support for token-based git authentication in identity profiles.
Enable discovery and import of credentials from ~/.git-credentials file.
Introduce remote URL-based filtering for token identities in git view.
This commit is contained in:
btriapitsyn
2026-01-14 02:47:26 +02:00
parent d56eef8aa6
commit fa83c1e645
16 changed files with 704 additions and 44 deletions
+126 -3
View File
@@ -187,11 +187,20 @@ pub struct GitIdentityProfile {
pub name: String,
pub user_name: String,
pub user_email: String,
pub auth_type: Option<String>,
pub ssh_key: Option<String>,
pub host: Option<String>,
pub color: Option<String>,
pub icon: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveredGitCredential {
pub host: String,
pub username: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct GitIdentityProfilesWrapper {
@@ -1984,6 +1993,22 @@ pub async fn delete_git_identity(id: String) -> Result<(), String> {
Ok(())
}
#[tauri::command]
pub async fn get_remote_url(
directory: String,
remote: Option<String>,
state: State<'_, DesktopRuntime>,
) -> Result<Option<String>, String> {
let root = validate_git_path(&directory, state.settings())
.await
.map_err(|e| e.to_string())?;
let remote_name = remote.unwrap_or_else(|| "origin".to_string());
let url = run_git(&["remote", "get-url", &remote_name], &root).await.ok();
Ok(url.filter(|s| !s.is_empty()))
}
#[tauri::command]
pub async fn get_current_git_identity(
directory: String,
@@ -2004,6 +2029,43 @@ pub async fn get_current_git_identity(
})
}
#[tauri::command]
pub async fn get_global_git_identity() -> Result<GitIdentitySummary, String> {
// Run git config --global commands without a specific directory
let user_name = tokio::process::Command::new("git")
.args(["config", "--global", "user.name"])
.output()
.await
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let user_email = tokio::process::Command::new("git")
.args(["config", "--global", "user.email"])
.output()
.await
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let ssh_command = tokio::process::Command::new("git")
.args(["config", "--global", "core.sshCommand"])
.output()
.await
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
Ok(GitIdentitySummary {
user_name,
user_email,
ssh_command,
})
}
#[tauri::command]
pub async fn set_git_identity(
directory: String,
@@ -2033,11 +2095,25 @@ pub async fn set_git_identity(
.await
.map_err(|e| e.to_string())?;
if let Some(key) = &profile.ssh_key {
let cmd = format!("ssh -i {}", key);
run_git(&["config", "--local", "core.sshCommand", &cmd], &root)
let auth_type = profile.auth_type.as_deref().unwrap_or("ssh");
if auth_type == "ssh" {
if let Some(key) = &profile.ssh_key {
let cmd = format!("ssh -i {}", key);
run_git(&["config", "--local", "core.sshCommand", &cmd], &root)
.await
.map_err(|e| e.to_string())?;
}
// Clear credential helper if previously set for token auth
let _ = run_git(&["config", "--local", "--unset", "credential.helper"], &root).await;
} else if auth_type == "token" && profile.host.is_some() {
// For token auth, configure git to use the store credential helper
// which reads from ~/.git-credentials
run_git(&["config", "--local", "credential.helper", "store"], &root)
.await
.map_err(|e| e.to_string())?;
// Clear SSH command if previously set
let _ = run_git(&["config", "--local", "--unset", "core.sshCommand"], &root).await;
} else {
let _ = run_git(&["config", "--local", "--unset", "core.sshCommand"], &root).await;
}
@@ -2045,6 +2121,53 @@ pub async fn set_git_identity(
Ok(profile)
}
#[tauri::command]
pub async fn discover_git_credentials() -> Result<Vec<DiscoveredGitCredential>, String> {
let home = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?;
let credentials_path = home.join(".git-credentials");
if !credentials_path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&credentials_path)
.await
.map_err(|e| format!("Failed to read .git-credentials: {}", e))?;
let mut credentials = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
// Parse URL format: https://username:token@host/path
if let Ok(url) = url::Url::parse(trimmed) {
let hostname = url.host_str().unwrap_or("").to_string();
let path = url.path();
// Include path for repo-specific tokens (e.g., github.com/user/repo)
let host = if path.is_empty() || path == "/" {
hostname
} else {
format!("{}{}", hostname, path)
};
let username = url.username().to_string();
if !host.is_empty() && !username.is_empty() {
// Avoid duplicates
let exists = credentials
.iter()
.any(|c: &DiscoveredGitCredential| c.host == host && c.username == username);
if !exists {
credentials.push(DiscoveredGitCredential { host, username });
}
}
}
}
Ok(credentials)
}
#[tauri::command]
pub async fn generate_commit_message(
directory: String,
+7 -4
View File
@@ -32,11 +32,11 @@ use commands::files::{create_directory, exec_commands, list_directory, read_file
use commands::git::{
add_git_worktree, check_is_git_repository, checkout_branch, create_branch, create_git_commit, rename_branch,
create_git_identity, delete_git_branch, delete_git_identity, delete_remote_branch,
ensure_openchamber_ignored, generate_commit_message, get_commit_files,
discover_git_credentials, ensure_openchamber_ignored, generate_commit_message, get_commit_files,
get_current_git_identity, get_git_branches, get_git_diff, get_git_file_diff,
get_git_identities, get_git_log, get_git_status, git_fetch, git_pull, git_push,
is_linked_worktree, list_git_worktrees, remove_git_worktree, revert_git_file, set_git_identity,
update_git_identity,
get_git_identities, get_git_log, get_git_status, get_global_git_identity, get_remote_url,
git_fetch, git_pull, git_push, is_linked_worktree, list_git_worktrees, remove_git_worktree,
revert_git_file, set_git_identity, update_git_identity,
};
use commands::logs::fetch_desktop_logs;
@@ -871,7 +871,10 @@ fn main() {
update_git_identity,
delete_git_identity,
get_current_git_identity,
get_global_git_identity,
get_remote_url,
set_git_identity,
discover_git_credentials,
generate_commit_message,
create_terminal_session,
send_terminal_input,