feat: implement default Git identity management and local identity check

This commit is contained in:
Bohdan Triapitsyn
2026-01-14 22:34:17 +02:00
parent e0cc212823
commit e0279e59b0
18 changed files with 310 additions and 25 deletions
+1 -1
View File
@@ -2977,7 +2977,7 @@ dependencies = [
[[package]]
name = "openchamber-desktop"
version = "1.4.8"
version = "1.4.9"
dependencies = [
"anyhow",
"axum",
@@ -2029,6 +2029,27 @@ pub async fn get_current_git_identity(
})
}
#[tauri::command]
pub async fn has_local_identity(
directory: String,
state: State<'_, DesktopRuntime>,
) -> Result<bool, String> {
let root = validate_git_path(&directory, state.settings())
.await
.map_err(|e| e.to_string())?;
let user_name = run_git(&["config", "--local", "--get", "user.name"], &root)
.await
.ok()
.filter(|s| !s.is_empty());
let user_email = run_git(&["config", "--local", "--get", "user.email"], &root)
.await
.ok()
.filter(|s| !s.is_empty());
Ok(user_name.is_some() || user_email.is_some())
}
#[tauri::command]
pub async fn get_global_git_identity() -> Result<GitIdentitySummary, String> {
let user_name = tokio::process::Command::new("git")
@@ -257,6 +257,14 @@ fn sanitize_settings_update(payload: &Value) -> Value {
result_obj.insert("defaultAgent".to_string(), json!(trimmed));
}
}
if let Some(Value::String(s)) = obj.get("defaultGitIdentityId") {
let trimmed = s.trim();
if trimmed.is_empty() {
result_obj.insert("defaultGitIdentityId".to_string(), Value::Null);
} else {
result_obj.insert("defaultGitIdentityId".to_string(), json!(trimmed));
}
}
if let Some(Value::String(s)) = obj.get("commitMessageModel") {
let trimmed = s.trim();
if trimmed.is_empty() {
+2 -1
View File
@@ -33,7 +33,7 @@ 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,
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_current_git_identity, has_local_identity, get_git_branches, get_git_diff, get_git_file_diff,
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,
@@ -871,6 +871,7 @@ fn main() {
update_git_identity,
delete_git_identity,
get_current_git_identity,
has_local_identity,
get_global_git_identity,
get_remote_url,
set_git_identity,
+8
View File
@@ -218,6 +218,14 @@ export const createDesktopGitAPI = (): GitAPI => ({
}
},
async hasLocalIdentity(directory: string): Promise<boolean> {
try {
return await safeGitInvoke<boolean>('has_local_identity', { directory });
} catch {
return false;
}
},
async setGitIdentity(directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }> {
const profile = await safeGitInvoke<GitIdentityProfile>('set_git_identity', { directory, profileId });
return { success: true, profile };
@@ -51,6 +51,7 @@ interface GitIdentitiesSidebarProps {
export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onItemSelect }) => {
const {
selectedProfileId,
defaultGitIdentityId,
profiles,
globalIdentity,
setSelectedProfile,
@@ -58,6 +59,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
loadProfiles,
loadGlobalIdentity,
loadDiscoveredCredentials,
loadDefaultGitIdentityId,
setDefaultGitIdentityId,
getUnimportedCredentials,
} = useGitIdentitiesStore();
@@ -82,7 +85,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
loadProfiles();
loadGlobalIdentity();
loadDiscoveredCredentials();
}, [loadProfiles, loadGlobalIdentity, loadDiscoveredCredentials]);
loadDefaultGitIdentityId();
}, [loadProfiles, loadGlobalIdentity, loadDiscoveredCredentials, loadDefaultGitIdentityId]);
const handleImportCredential = (credential: DiscoveredGitCredential) => {
// Set a special "import" selection that carries the credential data
@@ -119,6 +123,16 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
}
};
const handleToggleDefault = async (profileId: string) => {
const next = defaultGitIdentityId === profileId ? null : profileId;
const ok = await setDefaultGitIdentityId(next);
if (!ok) {
toast.error('Failed to update default identity');
return;
}
toast.success(next ? 'Default identity updated' : 'Default identity unset');
};
return (
<div className={cn('flex h-full flex-col', bgClass)}>
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
@@ -146,6 +160,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
<ProfileListItem
profile={globalIdentity}
isSelected={selectedProfileId === 'global'}
isDefault={defaultGitIdentityId === 'global'}
onSelect={() => {
setSelectedProfile('global');
onItemSelect?.();
@@ -153,6 +168,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
setSidebarOpen(false);
}
}}
onToggleDefault={() => handleToggleDefault('global')}
onDelete={undefined}
isReadOnly
/>
@@ -179,6 +195,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
key={profile.id}
profile={profile}
isSelected={selectedProfileId === profile.id}
isDefault={defaultGitIdentityId === profile.id}
onSelect={() => {
setSelectedProfile(profile.id);
onItemSelect?.();
@@ -186,6 +203,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
setSidebarOpen(false);
}
}}
onToggleDefault={() => handleToggleDefault(profile.id)}
onDelete={() => handleDeleteProfile(profile)}
/>
))}
@@ -218,7 +236,9 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
interface ProfileListItemProps {
profile: GitIdentityProfile;
isSelected: boolean;
isDefault?: boolean;
onSelect: () => void;
onToggleDefault?: () => void | Promise<void>;
onDelete?: () => void;
isReadOnly?: boolean;
}
@@ -226,7 +246,9 @@ interface ProfileListItemProps {
const ProfileListItem: React.FC<ProfileListItemProps> = ({
profile,
isSelected,
isDefault = false,
onSelect,
onToggleDefault,
onDelete,
isReadOnly = false,
}) => {
@@ -258,6 +280,11 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
<span className="typography-micro text-muted-foreground bg-muted px-1 rounded flex-shrink-0 leading-none pb-px border border-border/50">
{authType}
</span>
{isDefault && (
<span className="typography-micro text-primary bg-primary/12 px-1 rounded flex-shrink-0 leading-none pb-px border border-primary/25">
default
</span>
)}
</div>
<div className="typography-micro text-muted-foreground/60 truncate leading-tight">
@@ -265,7 +292,7 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
</div>
</button>
{!isReadOnly && onDelete && (
{(onToggleDefault || (!isReadOnly && onDelete)) && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -276,17 +303,29 @@ const ProfileListItem: React.FC<ProfileListItemProps> = ({
<RiMore2Line className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit min-w-20">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="text-destructive focus:text-destructive"
>
<RiDeleteBinLine className="h-4 w-4 mr-px" />
Delete
</DropdownMenuItem>
<DropdownMenuContent align="end" className="w-fit min-w-28">
{onToggleDefault && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
void onToggleDefault();
}}
>
{isDefault ? 'Unset default' : 'Set as default'}
</DropdownMenuItem>
)}
{!isReadOnly && onDelete && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="text-destructive focus:text-destructive"
>
<RiDeleteBinLine className="h-4 w-4 mr-px" />
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
@@ -25,6 +25,7 @@ import { getDesktopSettings, isDesktopRuntime, isVSCodeRuntime } from '@/lib/des
import { updateDesktopSettings } from '@/lib/persistence';
import type { DesktopSettings, SkillCatalogConfig } from '@/lib/desktop';
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
const generateCatalogId = () => `custom:${Date.now()}-${Math.random().toString(16).slice(2)}`;
@@ -81,6 +82,8 @@ interface AddCatalogDialogProps {
export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpenChange }) => {
const { scanRepo, loadCatalog, isScanning } = useSkillsCatalogStore();
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
const loadDefaultGitIdentityId = useGitIdentitiesStore((s) => s.loadDefaultGitIdentityId);
const [label, setLabel] = React.useState('');
const [source, setSource] = React.useState('');
@@ -104,13 +107,14 @@ export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpen
setScanOk(false);
setIdentityOptions([]);
setGitIdentityId(null);
void loadDefaultGitIdentityId();
void (async () => {
const settings = await loadSettings();
const catalogs = Array.isArray(settings?.skillCatalogs) ? settings?.skillCatalogs : [];
setExistingCatalogs(catalogs || []);
})();
}, [open]);
}, [open, loadDefaultGitIdentityId]);
const isDuplicate = React.useMemo(() => {
const normalizedSource = source.trim();
@@ -153,7 +157,13 @@ export const AddCatalogDialog: React.FC<AddCatalogDialogProps> = ({ open, onOpen
const ids = (result.error.identities || []) as IdentityOption[];
setIdentityOptions(ids);
if (!gitIdentityId && ids.length > 0) {
setGitIdentityId(ids[0].id);
const preferred =
defaultGitIdentityId &&
defaultGitIdentityId !== 'global' &&
ids.some((i) => i.id === defaultGitIdentityId)
? defaultGitIdentityId
: ids[0].id;
setGitIdentityId(preferred);
}
toast.error('Authentication required. Select a Git identity and scan again.');
return;
@@ -25,6 +25,7 @@ import { isVSCodeRuntime } from '@/lib/desktop';
import type { SkillsCatalogItem } from '@/lib/api/types';
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
import { useSkillsStore } from '@/stores/useSkillsStore';
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
import { InstallConflictsDialog, type ConflictDecision, type SkillConflict } from './InstallConflictsDialog';
interface InstallFromRepoDialogProps {
@@ -37,6 +38,8 @@ type IdentityOption = { id: string; name: string };
export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ open, onOpenChange }) => {
const { scanRepo, installSkills, isScanning, isInstalling } = useSkillsCatalogStore();
const installedSkills = useSkillsStore((s) => s.skills);
const defaultGitIdentityId = useGitIdentitiesStore((s) => s.defaultGitIdentityId);
const loadDefaultGitIdentityId = useGitIdentitiesStore((s) => s.loadDefaultGitIdentityId);
const [source, setSource] = React.useState('');
const [subpath, setSubpath] = React.useState('');
@@ -69,10 +72,13 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
setSearch('');
setIdentities([]);
setGitIdentityId(null);
void loadDefaultGitIdentityId();
setConflictsOpen(false);
setConflicts([]);
setBaseInstallRequest(null);
}, [open]);
}, [open, loadDefaultGitIdentityId]);
const installedByName = React.useMemo(() => {
const map = new Map<string, { scope: 'user' | 'project' }>();
@@ -127,7 +133,13 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
const ids = (result.error.identities || []) as IdentityOption[];
setIdentities(ids);
if (!gitIdentityId && ids.length > 0) {
setGitIdentityId(ids[0].id);
const preferred =
defaultGitIdentityId &&
defaultGitIdentityId !== 'global' &&
ids.some((i) => i.id === defaultGitIdentityId)
? defaultGitIdentityId
: ids[0].id;
setGitIdentityId(preferred);
}
toast.error('Authentication required. Select a Git identity and try scanning again.');
return;
@@ -195,7 +207,13 @@ export const InstallFromRepoDialog: React.FC<InstallFromRepoDialogProps> = ({ op
const ids = (result.error.identities || []) as IdentityOption[];
setIdentities(ids);
if (!gitIdentityId && ids.length > 0) {
setGitIdentityId(ids[0].id);
const preferred =
defaultGitIdentityId &&
defaultGitIdentityId !== 'global' &&
ids.some((i) => i.id === defaultGitIdentityId)
? defaultGitIdentityId
: ids[0].id;
setGitIdentityId(preferred);
}
toast.error('Authentication required. Select a Git identity and try installing again.');
return;
+59 -4
View File
@@ -60,7 +60,7 @@ export const GitView: React.FC = () => {
? worktreeMap.get(currentSessionId) ?? undefined
: undefined;
const { profiles, globalIdentity, loadProfiles, loadGlobalIdentity } =
const { profiles, globalIdentity, defaultGitIdentityId, loadProfiles, loadGlobalIdentity, loadDefaultGitIdentityId } =
useGitIdentitiesStore();
const isGitRepo = useIsGitRepo(currentDirectory ?? null);
@@ -95,6 +95,21 @@ export const GitView: React.FC = () => {
const [isSettingIdentity, setIsSettingIdentity] = React.useState(false);
const { triggerFireworks } = useFireworksCelebration();
const autoAppliedDefaultRef = React.useRef<Map<string, string>>(new Map());
const identityApplyCountRef = React.useRef(0);
const beginIdentityApply = React.useCallback(() => {
identityApplyCountRef.current += 1;
setIsSettingIdentity(true);
}, []);
const endIdentityApply = React.useCallback(() => {
identityApplyCountRef.current = Math.max(0, identityApplyCountRef.current - 1);
if (identityApplyCountRef.current === 0) {
setIsSettingIdentity(false);
}
}, []);
const [selectedPaths, setSelectedPaths] = React.useState<Set<string>>(
() => new Set(initialSnapshot?.selectedPaths ?? [])
);
@@ -187,7 +202,8 @@ export const GitView: React.FC = () => {
React.useEffect(() => {
loadProfiles();
loadGlobalIdentity();
}, [loadProfiles, loadGlobalIdentity]);
loadDefaultGitIdentityId();
}, [loadProfiles, loadGlobalIdentity, loadDefaultGitIdentityId]);
React.useEffect(() => {
if (!currentDirectory || !git?.getRemoteUrl) {
@@ -238,6 +254,45 @@ export const GitView: React.FC = () => {
await fetchIdentity(currentDirectory, git);
}, [currentDirectory, git, fetchIdentity]);
React.useEffect(() => {
if (!currentDirectory) return;
if (!git?.hasLocalIdentity) return;
if (isGitRepo !== true) return;
const defaultId = typeof defaultGitIdentityId === 'string' ? defaultGitIdentityId.trim() : '';
if (!defaultId || defaultId === 'global') return;
const previousAttempt = autoAppliedDefaultRef.current.get(currentDirectory);
if (previousAttempt === defaultId) return;
let cancelled = false;
const run = async () => {
try {
const hasLocal = await git.hasLocalIdentity!(currentDirectory);
if (cancelled) return;
if (hasLocal) return;
beginIdentityApply();
await git.setGitIdentity(currentDirectory, defaultId);
autoAppliedDefaultRef.current.set(currentDirectory, defaultId);
await refreshIdentity();
} catch (error) {
console.warn('Failed to auto-apply default git identity:', error);
} finally {
if (!cancelled) {
endIdentityApply();
}
}
};
void run();
return () => {
cancelled = true;
};
}, [beginIdentityApply, currentDirectory, defaultGitIdentityId, endIdentityApply, git, isGitRepo, refreshIdentity]);
const changeEntries = React.useMemo(() => {
if (!status) return [];
const files = status.files ?? [];
@@ -470,7 +525,7 @@ export const GitView: React.FC = () => {
const handleApplyIdentity = async (profile: GitIdentityProfile) => {
if (!currentDirectory) return;
setIsSettingIdentity(true);
beginIdentityApply();
try {
await git.setGitIdentity(currentDirectory, profile.id);
@@ -480,7 +535,7 @@ export const GitView: React.FC = () => {
const message = err instanceof Error ? err.message : 'Failed to apply git identity';
toast.error(message);
} finally {
setIsSettingIdentity(false);
endIdentityApply();
}
};
+1
View File
@@ -294,6 +294,7 @@ export interface GitAPI {
getGitLog(directory: string, options?: GitLogOptions): Promise<GitLogResponse>;
getCommitFiles(directory: string, hash: string): Promise<GitCommitFilesResponse>;
getCurrentGitIdentity(directory: string): Promise<GitIdentitySummary | null>;
hasLocalIdentity?(directory: string): Promise<boolean>;
setGitIdentity(directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }>;
getGitIdentities(): Promise<GitIdentityProfile[]>;
createGitIdentity(profile: GitIdentityProfile): Promise<GitIdentityProfile>;
+1
View File
@@ -56,6 +56,7 @@ export type DesktopSettings = {
defaultModel?: string; // format: "provider/model"
defaultVariant?: string;
defaultAgent?: string;
defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id
autoCreateWorktree?: boolean;
queueModeEnabled?: boolean;
commitMessageModel?: string; // format: "provider/model"
+6
View File
@@ -239,6 +239,12 @@ export async function getCurrentGitIdentity(directory: string): Promise<import('
return gitHttp.getCurrentGitIdentity(directory);
}
export async function hasLocalIdentity(directory: string): Promise<boolean> {
const runtime = getRuntimeGit();
if (runtime?.hasLocalIdentity) return runtime.hasLocalIdentity(directory);
return gitHttp.hasLocalIdentity(directory);
}
export async function setGitIdentity(
directory: string,
profileId: string
+12
View File
@@ -516,6 +516,18 @@ export async function getCurrentGitIdentity(directory: string): Promise<GitIdent
};
}
export async function hasLocalIdentity(directory: string): Promise<boolean> {
if (!directory) {
return false;
}
const response = await fetch(buildUrl(`${API_BASE}/has-local-identity`, directory));
if (!response.ok) {
throw new Error(`Failed to check local identity: ${response.statusText}`);
}
const data = await response.json().catch(() => null);
return data?.hasLocalIdentity === true;
}
export async function getGlobalGitIdentity(): Promise<GitIdentitySummary | null> {
const response = await fetch(buildUrl(`${API_BASE}/global-identity`, undefined));
if (!response.ok) {
@@ -10,6 +10,9 @@ import {
discoverGitCredentials,
getGlobalGitIdentity
} from "@/lib/gitApi";
import { getDesktopSettings, isDesktopRuntime } from "@/lib/desktop";
import { updateDesktopSettings } from "@/lib/persistence";
import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry";
export type GitIdentityAuthType = 'ssh' | 'token';
@@ -33,6 +36,7 @@ export interface DiscoveredGitCredential {
interface GitIdentitiesStore {
selectedProfileId: string | null;
defaultGitIdentityId: string | null; // null = unset, 'global' = system, profile id = custom
profiles: GitIdentityProfile[];
globalIdentity: GitIdentityProfile | null;
discoveredCredentials: DiscoveredGitCredential[];
@@ -42,6 +46,9 @@ interface GitIdentitiesStore {
loadProfiles: () => Promise<boolean>;
loadGlobalIdentity: () => Promise<boolean>;
loadDiscoveredCredentials: () => Promise<boolean>;
loadDefaultGitIdentityId: () => Promise<boolean>;
setDefaultGitIdentityId: (id: string | null) => Promise<boolean>;
createProfile: (profile: Omit<GitIdentityProfile, 'id'> & { id?: string }) => Promise<boolean>;
updateProfile: (id: string, updates: Partial<GitIdentityProfile>) => Promise<boolean>;
deleteProfile: (id: string) => Promise<boolean>;
@@ -61,6 +68,7 @@ export const useGitIdentitiesStore = create<GitIdentitiesStore>()(
(set, get) => ({
selectedProfileId: null,
defaultGitIdentityId: null,
profiles: [],
globalIdentity: null,
discoveredCredentials: [],
@@ -125,6 +133,70 @@ export const useGitIdentitiesStore = create<GitIdentitiesStore>()(
}
},
loadDefaultGitIdentityId: async () => {
const normalize = (value: unknown): string | null => {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
};
try {
let defaultId: string | null = null;
if (isDesktopRuntime()) {
const settings = await getDesktopSettings();
defaultId = normalize((settings as { defaultGitIdentityId?: unknown } | null | undefined)?.defaultGitIdentityId);
} else {
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
if (runtimeSettings) {
try {
const result = await runtimeSettings.load();
const settings = (result?.settings || {}) as Record<string, unknown>;
defaultId = normalize(settings.defaultGitIdentityId);
} catch {
// fall through
}
}
if (defaultId === null) {
try {
const response = await fetch('/api/config/settings', {
method: 'GET',
headers: { Accept: 'application/json' },
});
if (response.ok) {
const data = (await response.json().catch(() => null)) as Record<string, unknown> | null;
defaultId = normalize(data?.defaultGitIdentityId);
}
} catch {
// ignore
}
}
}
set({ defaultGitIdentityId: defaultId });
return true;
} catch (error) {
console.error('Failed to load default git identity setting:', error);
return false;
}
},
setDefaultGitIdentityId: async (id) => {
try {
const trimmed = typeof id === 'string' ? id.trim() : '';
const value = trimmed.length > 0 ? trimmed : '';
await updateDesktopSettings({ defaultGitIdentityId: value });
set({ defaultGitIdentityId: value.length > 0 ? value : null });
return true;
} catch (error) {
console.error('Failed to save default git identity setting:', error);
return false;
}
},
createProfile: async (profileData) => {
try {
+1 -1
View File
@@ -82,7 +82,7 @@ const persistSettings = async (changes: Record<string, unknown>, ctx?: BridgeCon
delete restChanges.lastDirectory;
// Normalize empty-string clears to key removal (match web/desktop behavior)
for (const key of ['defaultModel', 'defaultVariant', 'defaultAgent']) {
for (const key of ['defaultModel', 'defaultVariant', 'defaultAgent', 'defaultGitIdentityId']) {
const value = restChanges[key];
if (typeof value === 'string' && value.trim().length === 0) {
delete restChanges[key];
+20
View File
@@ -605,6 +605,10 @@ const sanitizeSettingsUpdate = (payload) => {
const trimmed = candidate.defaultAgent.trim();
result.defaultAgent = trimmed.length > 0 ? trimmed : undefined;
}
if (typeof candidate.defaultGitIdentityId === 'string') {
const trimmed = candidate.defaultGitIdentityId.trim();
result.defaultGitIdentityId = trimmed.length > 0 ? trimmed : undefined;
}
if (typeof candidate.queueModeEnabled === 'boolean') {
result.queueModeEnabled = candidate.queueModeEnabled;
}
@@ -3366,6 +3370,22 @@ async function main(options = {}) {
}
});
app.get('/api/git/has-local-identity', async (req, res) => {
const { hasLocalIdentity } = await getGitLibraries();
try {
const directory = req.query.directory;
if (!directory) {
return res.status(400).json({ error: 'directory parameter is required' });
}
const hasLocal = await hasLocalIdentity(directory);
res.json({ hasLocalIdentity: hasLocal });
} catch (error) {
console.error('Failed to check local git identity:', error);
res.status(500).json({ error: 'Failed to check local git identity' });
}
});
app.post('/api/git/set-identity', async (req, res) => {
const { getProfile, setLocalIdentity, getGlobalIdentity } = await getGitLibraries();
try {
+12
View File
@@ -160,6 +160,18 @@ export async function getCurrentIdentity(directory) {
}
}
export async function hasLocalIdentity(directory) {
const git = simpleGit(normalizeDirectoryPath(directory));
try {
const localName = await git.getConfig('user.name', 'local').catch(() => null);
const localEmail = await git.getConfig('user.email', 'local').catch(() => null);
return Boolean(localName?.value || localEmail?.value);
} catch {
return false;
}
}
export async function setLocalIdentity(directory, profile) {
const git = simpleGit(normalizeDirectoryPath(directory));
+1
View File
@@ -34,6 +34,7 @@ export const createWebGitAPI = (): GitAPI => ({
},
getCommitFiles: gitApiHttp.getCommitFiles,
getCurrentGitIdentity: gitApiHttp.getCurrentGitIdentity,
hasLocalIdentity: gitApiHttp.hasLocalIdentity,
setGitIdentity: gitApiHttp.setGitIdentity,
getGitIdentities: gitApiHttp.getGitIdentities,
createGitIdentity: gitApiHttp.createGitIdentity,