Browse Source
Nostr-Signature: d330d1a096e5f3951e8b2a66160a23c5ac28aa94313ecd0948c7e50baa60bdbb 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc febf4088cca3f7223f55ab300ed7fdb7b333c03d2534b05721dfaf9d9284f4599b385ba54379890fa6b846aed02d656a5e45429a6dd571dddbb997be6d8159b2main
5 changed files with 336 additions and 235 deletions
@ -0,0 +1,145 @@ |
|||||||
|
/** |
||||||
|
* 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> { |
||||||
|
state.loading.commits = true; |
||||||
|
state.error = null; |
||||||
|
try { |
||||||
|
const data = await apiRequest<Array<{ |
||||||
|
hash?: string; |
||||||
|
sha?: string; |
||||||
|
message?: string; |
||||||
|
author?: string; |
||||||
|
date?: string; |
||||||
|
files?: string[]; |
||||||
|
}>>(`/api/repos/${state.npub}/${state.repo}/commits?branch=${state.git.currentBranch}&limit=50`); |
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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) { |
||||||
|
state.error = err instanceof Error ? err.message : 'Failed to load commit history'; |
||||||
|
} 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}/verify`); |
||||||
|
|
||||||
|
// 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); |
||||||
|
const parentHash = commitIndex >= 0 |
||||||
|
? (state.git.commits[commitIndex + 1] ? getCommitHash(state.git.commits[commitIndex + 1]) : `${commitHash}^`) |
||||||
|
: `${commitHash}^`; |
||||||
|
|
||||||
|
const diffData = await apiRequest<Array<{ |
||||||
|
file: string; |
||||||
|
additions: number; |
||||||
|
deletions: number; |
||||||
|
diff: string; |
||||||
|
}>>(`/api/repos/${state.npub}/${state.repo}/diff?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; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,93 @@ |
|||||||
|
/** |
||||||
|
* Release operations service |
||||||
|
* Handles release loading and creation |
||||||
|
*/ |
||||||
|
|
||||||
|
import type { RepoState } from '../stores/repo-state.js'; |
||||||
|
import { apiRequest, apiPost } from '../utils/api-client.js'; |
||||||
|
import type { NostrEvent } from '$lib/types/nostr.js'; |
||||||
|
|
||||||
|
interface ReleaseOperationsCallbacks { |
||||||
|
loadReleases: () => Promise<void>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Load releases from the repository |
||||||
|
*/ |
||||||
|
export async function loadReleases( |
||||||
|
state: RepoState, |
||||||
|
callbacks: ReleaseOperationsCallbacks |
||||||
|
): Promise<void> { |
||||||
|
if (state.repoNotFound) return; |
||||||
|
state.loading.releases = true; |
||||||
|
try { |
||||||
|
const data = await apiRequest<Array<NostrEvent>>( |
||||||
|
`/api/repos/${state.npub}/${state.repo}/releases` |
||||||
|
); |
||||||
|
state.releases = data.map((release: any) => ({ |
||||||
|
id: release.id, |
||||||
|
tagName: release.tags.find((t: string[]) => t[0] === 'tag')?.[1] || '', |
||||||
|
tagHash: release.tags.find((t: string[]) => t[0] === 'r' && t[2] === 'tag')?.[1], |
||||||
|
releaseNotes: release.content || '', |
||||||
|
isDraft: release.tags.some((t: string[]) => t[0] === 'draft' && t[1] === 'true'), |
||||||
|
isPrerelease: release.tags.some((t: string[]) => t[0] === 'prerelease' && t[1] === 'true'), |
||||||
|
created_at: release.created_at, |
||||||
|
pubkey: release.pubkey |
||||||
|
})); |
||||||
|
} catch (err) { |
||||||
|
console.error('Failed to load releases:', err); |
||||||
|
} finally { |
||||||
|
state.loading.releases = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new release |
||||||
|
*/ |
||||||
|
export async function createRelease( |
||||||
|
state: RepoState, |
||||||
|
repoOwnerPubkeyDerived: string, |
||||||
|
callbacks: ReleaseOperationsCallbacks |
||||||
|
): Promise<void> { |
||||||
|
if (!state.forms.release.tagName.trim() || !state.forms.release.tagHash.trim()) { |
||||||
|
alert('Please enter a tag name and tag hash'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!state.user.pubkey) { |
||||||
|
alert('Please connect your NIP-07 extension'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!state.maintainers.isMaintainer && state.user.pubkeyHex !== repoOwnerPubkeyDerived) { |
||||||
|
alert('Only repository owners and maintainers can create releases'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
state.creating.release = true; |
||||||
|
state.error = null; |
||||||
|
|
||||||
|
try { |
||||||
|
await apiPost(`/api/repos/${state.npub}/${state.repo}/releases`, { |
||||||
|
tagName: state.forms.release.tagName, |
||||||
|
tagHash: state.forms.release.tagHash, |
||||||
|
releaseNotes: state.forms.release.notes, |
||||||
|
isDraft: state.forms.release.isDraft, |
||||||
|
isPrerelease: state.forms.release.isPrerelease |
||||||
|
}); |
||||||
|
|
||||||
|
state.openDialog = null; |
||||||
|
state.forms.release.tagName = ''; |
||||||
|
state.forms.release.tagHash = ''; |
||||||
|
state.forms.release.notes = ''; |
||||||
|
state.forms.release.isDraft = false; |
||||||
|
state.forms.release.isPrerelease = false; |
||||||
|
await callbacks.loadReleases(); |
||||||
|
alert('Release created successfully!'); |
||||||
|
} catch (err) { |
||||||
|
state.error = err instanceof Error ? err.message : 'Failed to create release'; |
||||||
|
alert(state.error); |
||||||
|
} finally { |
||||||
|
state.creating.release = false; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,73 @@ |
|||||||
|
/** |
||||||
|
* Tag operations service |
||||||
|
* Handles tag loading and creation |
||||||
|
*/ |
||||||
|
|
||||||
|
import type { RepoState } from '../stores/repo-state.js'; |
||||||
|
import { apiRequest, apiPost } from '../utils/api-client.js'; |
||||||
|
|
||||||
|
interface TagOperationsCallbacks { |
||||||
|
loadTags: () => Promise<void>; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Load tags from the repository |
||||||
|
*/ |
||||||
|
export async function loadTags( |
||||||
|
state: RepoState, |
||||||
|
callbacks: TagOperationsCallbacks |
||||||
|
): Promise<void> { |
||||||
|
if (state.repoNotFound) return; |
||||||
|
try { |
||||||
|
const tags = await apiRequest<Array<{ name: string; hash: string; message?: string; date?: number }>>( |
||||||
|
`/api/repos/${state.npub}/${state.repo}/tags` |
||||||
|
); |
||||||
|
state.git.tags = tags; |
||||||
|
// Auto-select first tag if none selected
|
||||||
|
if (state.git.tags.length > 0 && !state.git.selectedTag) { |
||||||
|
state.git.selectedTag = state.git.tags[0].name; |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
console.error('Failed to load tags:', err); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new tag |
||||||
|
*/ |
||||||
|
export async function createTag( |
||||||
|
state: RepoState, |
||||||
|
callbacks: TagOperationsCallbacks |
||||||
|
): Promise<void> { |
||||||
|
if (!state.forms.tag.name.trim()) { |
||||||
|
alert('Please enter a tag name'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!state.user.pubkey) { |
||||||
|
alert('Please connect your NIP-07 extension'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
state.saving = true; |
||||||
|
state.error = null; |
||||||
|
|
||||||
|
try { |
||||||
|
await apiPost(`/api/repos/${state.npub}/${state.repo}/tags`, { |
||||||
|
tagName: state.forms.tag.name, |
||||||
|
ref: state.forms.tag.ref, |
||||||
|
message: state.forms.tag.message || undefined, |
||||||
|
userPubkey: state.user.pubkey |
||||||
|
}); |
||||||
|
|
||||||
|
state.openDialog = null; |
||||||
|
state.forms.tag.name = ''; |
||||||
|
state.forms.tag.message = ''; |
||||||
|
await callbacks.loadTags(); |
||||||
|
alert('Tag created successfully!'); |
||||||
|
} catch (err) { |
||||||
|
state.error = err instanceof Error ? err.message : 'Failed to create tag'; |
||||||
|
} finally { |
||||||
|
state.saving = false; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue