Browse Source
Nostr-Signature: d330d1a096e5f3951e8b2a66160a23c5ac28aa94313ecd0948c7e50baa60bdbb 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc febf4088cca3f7223f55ab300ed7fdb7b333c03d2534b05721dfaf9d9284f4599b385ba54379890fa6b846aed02d656a5e45429a6dd571dddbb997be6d8159b2main
5 changed files with 336 additions and 235 deletions
@ -0,0 +1,145 @@
@@ -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 @@
@@ -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 @@
@@ -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