feat(git): enhance token authentication and credential handling
This commit is contained in:
+2
-2
@@ -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).
|
||||
|
||||
@@ -2031,7 +2031,6 @@ 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()
|
||||
@@ -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<Vec<DiscoveredGitCredential>,
|
||||
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<Vec<DiscoveredGitCredential>,
|
||||
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);
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user