From 1319033df9494a31b49b546f78863b73ff75ddeb Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 14 Jan 2026 02:54:48 +0200 Subject: [PATCH] feat(git): enhance token authentication and credential handling --- CHANGELOG.md | 4 ++-- .../desktop/src-tauri/src/commands/git.rs | 8 ------- .../git-identities/GitIdentitiesPage.tsx | 6 ++--- .../git-identities/GitIdentitiesSidebar.tsx | 7 ------ packages/ui/src/components/views/GitView.tsx | 23 +++++-------------- packages/web/server/lib/git-credentials.js | 15 +----------- packages/web/server/lib/git-service.js | 4 ---- 7 files changed, 11 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2baed65..76a743e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- Git Identities: added token-based authentication support with ~/.git-credentials discovery and import. - Settings: consolidated Git settings and added opencode zen model selection for commit generation (thanks to @nelsonpires). - Web Notifications: added configurable native web notifications for assistant completion (thanks to @vio1ator). - Chat: sidebar sessions are now automatically sorted by last updated date (thanks to @vio1ator). -- Chat: fixed edit tool output. -- Chat: added turn duration. +- Chat: fixed edit tool output and added turn duration. - UI: todo lists and status indicators now hide automatically when all tasks are completed (thanks to @vio1ator). - Reliability: improved project state preservation on validation failures (thanks to @vio1ator) and refined server health monitoring. - Stability: added graceful shutdown handling for the server process (thanks to @vio1ator). diff --git a/packages/desktop/src-tauri/src/commands/git.rs b/packages/desktop/src-tauri/src/commands/git.rs index ddfe528e..4fab1037 100644 --- a/packages/desktop/src-tauri/src/commands/git.rs +++ b/packages/desktop/src-tauri/src/commands/git.rs @@ -2031,7 +2031,6 @@ pub async fn get_current_git_identity( #[tauri::command] pub async fn get_global_git_identity() -> Result { - // Run git config --global commands without a specific directory let user_name = tokio::process::Command::new("git") .args(["config", "--global", "user.name"]) .output() @@ -2104,15 +2103,11 @@ pub async fn set_git_identity( .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; @@ -2141,11 +2136,9 @@ pub async fn discover_git_credentials() -> Result, 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 { @@ -2154,7 +2147,6 @@ pub async fn discover_git_credentials() -> Result, 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); diff --git a/packages/ui/src/components/sections/git-identities/GitIdentitiesPage.tsx b/packages/ui/src/components/sections/git-identities/GitIdentitiesPage.tsx index 12ed761a..3ca15e59 100644 --- a/packages/ui/src/components/sections/git-identities/GitIdentitiesPage.tsx +++ b/packages/ui/src/components/sections/git-identities/GitIdentitiesPage.tsx @@ -73,18 +73,16 @@ export const GitIdentitiesPage: React.FC = () => { React.useEffect(() => { if (importData) { - // Pre-fill from imported credential - // For repo-specific hosts like "github.com/user/repo", use just "repo" as name const parts = importData.host.split('/'); const displayName = parts.length >= 3 ? parts[parts.length - 1] : importData.host; - + setName(displayName); setUserName(importData.username); setUserEmail(''); setAuthType('token'); setSshKey(''); setHost(importData.host); - setColor('string'); // cyan for token-based + setColor('string'); setIcon('code'); } else if (isNewProfile) { setName(''); diff --git a/packages/ui/src/components/sections/git-identities/GitIdentitiesSidebar.tsx b/packages/ui/src/components/sections/git-identities/GitIdentitiesSidebar.tsx index be4059a2..99612ef3 100644 --- a/packages/ui/src/components/sections/git-identities/GitIdentitiesSidebar.tsx +++ b/packages/ui/src/components/sections/git-identities/GitIdentitiesSidebar.tsx @@ -300,18 +300,11 @@ interface DiscoveredCredentialItemProps { onImport: () => void; } -/** - * Get display name for a credential host. - * For repo-specific hosts like "github.com/user/repo", returns just "repo". - * For host-only like "github.com", returns "github.com". - */ const getCredentialDisplayName = (host: string): string => { const parts = host.split('/'); if (parts.length >= 3) { - // repo-specific: github.com/user/repo -> repo return parts[parts.length - 1]; } - // host-only: github.com return host; }; diff --git a/packages/ui/src/components/views/GitView.tsx b/packages/ui/src/components/views/GitView.tsx index 5cdf2165..050eb90f 100644 --- a/packages/ui/src/components/views/GitView.tsx +++ b/packages/ui/src/components/views/GitView.tsx @@ -189,7 +189,6 @@ export const GitView: React.FC = () => { loadGlobalIdentity(); }, [loadProfiles, loadGlobalIdentity]); - // Fetch remote URL for filtering token-based identities React.useEffect(() => { if (!currentDirectory || !git?.getRemoteUrl) { setRemoteUrl(null); @@ -505,49 +504,39 @@ export const GitView: React.FC = () => { if (globalIdentity) { unique.set(globalIdentity.id, globalIdentity); } - - // Parse repo host/path from remote URL for filtering token identities - // e.g., "git@github.com:user/repo.git" or "https://github.com/user/repo.git" + let repoHostPath: string | null = null; if (remoteUrl) { try { let normalized = remoteUrl.trim(); - // Handle SSH format: git@github.com:user/repo.git -> https://github.com/user/repo.git if (normalized.startsWith('git@')) { normalized = 'https://' + normalized.slice(4).replace(':', '/'); } - // Remove .git suffix if (normalized.endsWith('.git')) { normalized = normalized.slice(0, -4); } const url = new URL(normalized); repoHostPath = url.hostname + url.pathname; - } catch { - // ignore parse errors - } + } catch { /* ignore */ } } - + for (const profile of profiles) { - // SSH identities always shown if (profile.authType !== 'token') { unique.set(profile.id, profile); continue; } - - // Token identities: filter by host match + const profileHost = profile.host; if (!profileHost) { unique.set(profile.id, profile); continue; } - - // Host-only token (e.g., "github.com") - always show + if (!profileHost.includes('/')) { unique.set(profile.id, profile); continue; } - - // Repo-specific token - only show if matches current repo + if (repoHostPath && repoHostPath === profileHost) { unique.set(profile.id, profile); } diff --git a/packages/web/server/lib/git-credentials.js b/packages/web/server/lib/git-credentials.js index 10a2cc11..1607f142 100644 --- a/packages/web/server/lib/git-credentials.js +++ b/packages/web/server/lib/git-credentials.js @@ -4,11 +4,6 @@ import os from 'os'; const GIT_CREDENTIALS_PATH = path.join(os.homedir(), '.git-credentials'); -/** - * Parse ~/.git-credentials file and return discovered credentials. - * Format: https://username:token@host or https://username:token@host/path - * @returns {Array<{host: string, username: string}>} - */ export function discoverGitCredentials() { const credentials = []; @@ -25,19 +20,16 @@ export function discoverGitCredentials() { const url = new URL(line.trim()); const hostname = url.hostname; const pathname = url.pathname && url.pathname !== '/' ? url.pathname : ''; - // Include path for repo-specific tokens (e.g., github.com/user/repo) const host = hostname + pathname; const username = url.username || ''; if (host && username) { - // Avoid duplicates const exists = credentials.some(c => c.host === host && c.username === username); if (!exists) { credentials.push({ host, username }); } } } catch { - // Skip malformed lines continue; } } @@ -48,11 +40,6 @@ export function discoverGitCredentials() { return credentials; } -/** - * Get credential for a specific host from ~/.git-credentials - * @param {string} host - The host to look up (e.g., "github.com" or "github.com/user/repo") - * @returns {{username: string, token: string} | null} - */ export function getCredentialForHost(host) { if (!fs.existsSync(GIT_CREDENTIALS_PATH)) { return null; @@ -68,7 +55,7 @@ export function getCredentialForHost(host) { const hostname = url.hostname; const pathname = url.pathname && url.pathname !== '/' ? url.pathname : ''; const credHost = hostname + pathname; - + if (credHost === host) { return { username: url.username || '', diff --git a/packages/web/server/lib/git-service.js b/packages/web/server/lib/git-service.js index 8b7da662..5a902df7 100644 --- a/packages/web/server/lib/git-service.js +++ b/packages/web/server/lib/git-service.js @@ -177,18 +177,14 @@ export async function setLocalIdentity(directory, profile) { false, 'local' ); - // Clear credential helper if previously set for token auth await git.raw(['config', '--local', '--unset', 'credential.helper']).catch(() => {}); } else if (authType === 'token' && profile.host) { - // For token auth, configure git to use the store credential helper - // which reads from ~/.git-credentials await git.addConfig( 'credential.helper', 'store', false, 'local' ); - // Clear SSH command if previously set await git.raw(['config', '--local', '--unset', 'core.sshCommand']).catch(() => {}); }