You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
7.8 KiB
211 lines
7.8 KiB
/** |
|
* Commit operations service |
|
* Handles commit history loading, verification, and diff viewing |
|
*/ |
|
|
|
import type { RepoState } from '../stores/repo-state.js'; |
|
import { apiRequest } from '../utils/api-client.js'; |
|
|
|
interface CommitOperationsCallbacks { |
|
verifyCommit: (commitHash: string) => Promise<void>; |
|
} |
|
|
|
/** |
|
* Load commit history |
|
*/ |
|
export async function loadCommitHistory( |
|
state: RepoState, |
|
callbacks: CommitOperationsCallbacks |
|
): Promise<void> { |
|
// Skip if repo is not cloned and no API fallback available |
|
if (state.clone.isCloned === false && !state.clone.apiFallbackAvailable) { |
|
state.loading.commits = false; |
|
state.error = null; |
|
state.git.commits = []; |
|
console.log('[loadCommitHistory] Skipping - repo not cloned and no API fallback available'); |
|
return; |
|
} |
|
|
|
state.loading.commits = true; |
|
state.error = null; |
|
try { |
|
// Use currentBranch, fallback to defaultBranch, then 'master' |
|
const branch = state.git.currentBranch || state.git.defaultBranch || 'master'; |
|
const url = `/api/repos/${state.npub}/${state.repo}/commits?branch=${encodeURIComponent(branch)}&limit=50`; |
|
console.log('[loadCommitHistory] Fetching commits:', { url, branch, currentBranch: state.git.currentBranch, defaultBranch: state.git.defaultBranch }); |
|
|
|
const response = await apiRequest<Array<{ |
|
hash?: string; |
|
sha?: string; |
|
message?: string; |
|
author?: string; |
|
date?: string; |
|
files?: string[]; |
|
}> | { commitCount?: number; data?: Array<any> }>(url); |
|
|
|
// Handle both array and object response formats |
|
// API should return array, but handle object wrappers like { data: [] } or { commits: [] } |
|
let data: Array<any>; |
|
if (Array.isArray(response)) { |
|
data = response; |
|
} else if (response && typeof response === 'object') { |
|
// Try common wrapper formats |
|
data = (response as any).data || (response as any).commits || []; |
|
} else { |
|
data = []; |
|
} |
|
|
|
console.log('[loadCommitHistory] Received response:', { |
|
responseType: Array.isArray(response) ? 'array' : typeof response, |
|
responseKeys: typeof response === 'object' && response !== null ? Object.keys(response) : [], |
|
commitCount: data?.length || 0, |
|
data |
|
}); |
|
|
|
// Normalize commits: API-based commits use 'sha', local commits use 'hash' |
|
state.git.commits = data.map((commit: any) => ({ |
|
hash: commit.hash || commit.sha || '', |
|
message: commit.message || 'No message', |
|
author: commit.author || 'Unknown', |
|
date: commit.date || new Date().toISOString(), |
|
files: commit.files || [] |
|
})).filter((commit: any) => commit.hash); // Filter out commits without hash |
|
|
|
console.log('[loadCommitHistory] Normalized commits:', { count: state.git.commits.length }); |
|
|
|
// Verify commits in background (only for cloned repos) |
|
if (state.clone.isCloned === true) { |
|
state.git.commits.forEach(commit => { |
|
callbacks.verifyCommit(commit.hash).catch(err => { |
|
console.warn(`Failed to verify commit ${commit.hash}:`, err); |
|
}); |
|
}); |
|
} |
|
} catch (err) { |
|
console.error('[loadCommitHistory] Error loading commits:', err); |
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load commit history'; |
|
|
|
// Handle 404 gracefully - repo not cloned |
|
if (errorMessage.includes('404') || errorMessage.includes('not found') || errorMessage.includes('Repository not found')) { |
|
// If repo is not cloned, this is expected - don't set error |
|
if (state.clone.isCloned === false) { |
|
state.error = null; |
|
state.git.commits = []; |
|
console.log('[loadCommitHistory] Repo not cloned - commits unavailable'); |
|
} else { |
|
state.error = errorMessage; |
|
} |
|
} else { |
|
state.error = errorMessage; |
|
} |
|
} finally { |
|
state.loading.commits = false; |
|
} |
|
} |
|
|
|
/** |
|
* Verify a commit signature |
|
*/ |
|
export async function verifyCommit( |
|
commitHash: string, |
|
state: RepoState |
|
): Promise<void> { |
|
if (state.git.verifyingCommits.has(commitHash)) return; // Already verifying |
|
if (!state.clone.isCloned) return; // Can't verify without local repo |
|
|
|
state.git.verifyingCommits.add(commitHash); |
|
try { |
|
const verification = await apiRequest<{ |
|
valid: boolean; |
|
hasSignature?: boolean; |
|
error?: string; |
|
pubkey?: string; |
|
npub?: string; |
|
authorName?: string; |
|
authorEmail?: string; |
|
timestamp?: number; |
|
eventId?: string; |
|
}>(`/api/repos/${state.npub}/${state.repo}/commits/${commitHash}/verification`); |
|
|
|
// Only update verification if there's actually a signature |
|
// If hasSignature is false or undefined, don't set verification at all |
|
if (verification.hasSignature !== false) { |
|
const commitIndex = state.git.commits.findIndex(c => c.hash === commitHash); |
|
if (commitIndex >= 0) { |
|
state.git.commits[commitIndex].verification = verification; |
|
} |
|
} |
|
} catch (err) { |
|
console.warn(`Failed to verify commit ${commitHash}:`, err); |
|
} finally { |
|
state.git.verifyingCommits.delete(commitHash); |
|
} |
|
} |
|
|
|
/** |
|
* View diff for a commit |
|
*/ |
|
export async function viewDiff( |
|
commitHash: string, |
|
state: RepoState |
|
): Promise<void> { |
|
// Set selected commit immediately so it shows in the right panel |
|
state.git.selectedCommit = commitHash; |
|
state.git.showDiff = false; // Start with false, will be set to true when diff loads |
|
state.loading.commits = true; |
|
state.error = null; |
|
try { |
|
// Normalize commit hash (handle both 'hash' and 'sha' properties) |
|
const getCommitHash = (c: any) => c.hash || c.sha || ''; |
|
const commitIndex = state.git.commits.findIndex(c => getCommitHash(c) === commitHash); |
|
|
|
// Determine parent hash: if this is the last commit (initial commit), use empty tree |
|
// Otherwise, use the next commit in the list or the parent commit |
|
let parentHash: string; |
|
if (commitIndex >= 0) { |
|
// Check if this is the last commit (initial commit with no parent) |
|
if (commitIndex === state.git.commits.length - 1) { |
|
// This is the initial commit - use empty tree hash |
|
// Git's empty tree hash: 4b825dc642cb6eb9a060e54bf8d69288fbee4904 |
|
parentHash = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; |
|
} else { |
|
// Use the next commit (which is the parent in reverse chronological order) |
|
parentHash = getCommitHash(state.git.commits[commitIndex + 1]); |
|
} |
|
} else { |
|
// Commit not found in list, try to use parent (but this might fail for initial commit) |
|
// We'll let the API handle the error |
|
parentHash = `${commitHash}^`; |
|
} |
|
|
|
const diffData = await apiRequest<Array<{ |
|
file: string; |
|
additions: number; |
|
deletions: number; |
|
diff: string; |
|
}>>(`/api/repos/${state.npub}/${state.repo}/diffs?from=${parentHash}&to=${commitHash}`); |
|
|
|
state.git.diffData = diffData; |
|
state.git.showDiff = true; |
|
} catch (err) { |
|
// Handle 404 or other errors |
|
if (err instanceof Error) { |
|
if (err.message.includes('404') || err.message.includes('not found')) { |
|
// Check if this is an API fallback commit (repo not cloned or empty) |
|
if (state.clone.isCloned === false || (state.clone.isCloned === true && state.clone.apiFallbackAvailable)) { |
|
state.error = 'Diffs are not available for commits viewed via API fallback. Please clone the repository to view diffs.'; |
|
} else { |
|
state.error = `Commit not found: ${err.message || 'The commit may not exist in the repository'}`; |
|
} |
|
} else if (err.message.includes('NetworkError')) { |
|
state.error = 'Network error: Unable to fetch diff. Please check your connection and try again.'; |
|
} else { |
|
state.error = err.message || 'Failed to load diff'; |
|
} |
|
} else { |
|
state.error = 'Failed to load diff'; |
|
} |
|
} finally { |
|
state.loading.commits = false; |
|
} |
|
}
|
|
|