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:
@@ -3305,6 +3305,17 @@ async function main(options = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/git/discover-credentials', async (req, res) => {
|
||||
try {
|
||||
const { discoverGitCredentials } = await import('./lib/git-credentials.js');
|
||||
const credentials = discoverGitCredentials();
|
||||
res.json(credentials);
|
||||
} catch (error) {
|
||||
console.error('Failed to discover git credentials:', error);
|
||||
res.status(500).json({ error: 'Failed to discover git credentials' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/git/check', async (req, res) => {
|
||||
const { isGitRepository } = await getGitLibraries();
|
||||
try {
|
||||
@@ -3321,6 +3332,23 @@ async function main(options = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/git/remote-url', async (req, res) => {
|
||||
const { getRemoteUrl } = await getGitLibraries();
|
||||
try {
|
||||
const directory = req.query.directory;
|
||||
if (!directory) {
|
||||
return res.status(400).json({ error: 'directory parameter is required' });
|
||||
}
|
||||
const remote = req.query.remote || 'origin';
|
||||
|
||||
const url = await getRemoteUrl(directory, remote);
|
||||
res.json({ url });
|
||||
} catch (error) {
|
||||
console.error('Failed to get remote url:', error);
|
||||
res.status(500).json({ error: 'Failed to get remote url' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/git/current-identity', async (req, res) => {
|
||||
const { getCurrentIdentity } = await getGitLibraries();
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
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 = [];
|
||||
|
||||
if (!fs.existsSync(GIT_CREDENTIALS_PATH)) {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(GIT_CREDENTIALS_PATH, 'utf8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to read .git-credentials:', error);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(GIT_CREDENTIALS_PATH, 'utf8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const url = new URL(line.trim());
|
||||
const hostname = url.hostname;
|
||||
const pathname = url.pathname && url.pathname !== '/' ? url.pathname : '';
|
||||
const credHost = hostname + pathname;
|
||||
|
||||
if (credHost === host) {
|
||||
return {
|
||||
username: url.username || '',
|
||||
token: url.password || ''
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to read .git-credentials for host lookup:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -66,7 +66,9 @@ export function createProfile(profileData) {
|
||||
name: profileData.name || profileData.userName,
|
||||
userName: profileData.userName,
|
||||
userEmail: profileData.userEmail,
|
||||
authType: profileData.authType || 'ssh',
|
||||
sshKey: profileData.sshKey || null,
|
||||
host: profileData.host || null,
|
||||
color: profileData.color || 'keyword',
|
||||
icon: profileData.icon || 'branch'
|
||||
};
|
||||
|
||||
@@ -117,6 +117,17 @@ export async function getGlobalIdentity() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRemoteUrl(directory, remoteName = 'origin') {
|
||||
const git = simpleGit(normalizeDirectoryPath(directory));
|
||||
|
||||
try {
|
||||
const url = await git.remote(['get-url', remoteName]);
|
||||
return url?.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCurrentIdentity(directory) {
|
||||
const git = simpleGit(normalizeDirectoryPath(directory));
|
||||
|
||||
@@ -157,13 +168,28 @@ export async function setLocalIdentity(directory, profile) {
|
||||
await git.addConfig('user.name', profile.userName, false, 'local');
|
||||
await git.addConfig('user.email', profile.userEmail, false, 'local');
|
||||
|
||||
if (profile.sshKey) {
|
||||
const authType = profile.authType || 'ssh';
|
||||
|
||||
if (authType === 'ssh' && profile.sshKey) {
|
||||
await git.addConfig(
|
||||
'core.sshCommand',
|
||||
`ssh -i ${profile.sshKey}`,
|
||||
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(() => {});
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user