From 8984be75c154176efdd0b27140b47598535bc451 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 26 Feb 2026 13:52:17 +0100 Subject: [PATCH] refactor 4 Nostr-Signature: d330d1a096e5f3951e8b2a66160a23c5ac28aa94313ecd0948c7e50baa60bdbb 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc febf4088cca3f7223f55ab300ed7fdb7b333c03d2534b05721dfaf9d9284f4599b385ba54379890fa6b846aed02d656a5e45429a6dd571dddbb997be6d8159b2 --- nostr/commit-signatures.jsonl | 1 + src/routes/repos/[npub]/[repo]/+page.svelte | 259 ++---------------- .../[repo]/services/commit-operations.ts | 145 ++++++++++ .../[repo]/services/release-operations.ts | 93 +++++++ .../[npub]/[repo]/services/tag-operations.ts | 73 +++++ 5 files changed, 336 insertions(+), 235 deletions(-) create mode 100644 src/routes/repos/[npub]/[repo]/services/commit-operations.ts create mode 100644 src/routes/repos/[npub]/[repo]/services/release-operations.ts create mode 100644 src/routes/repos/[npub]/[repo]/services/tag-operations.ts diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 0fc925a..5890542 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -96,3 +96,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772106804,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor 3"]],"content":"Signed commit: refactor 3","id":"a761c789227ef2368eff89f7062fa7889820c4846701667360978cfdad08c3d2","sig":"9d229200ab66d3f4a0a2a21112c9100ee14d0a5d9f8409a35fef36f195f5f73c8ac2344aa1175cc476f650336a5a10ea6ac0076c8ec2cb229fea7d600c5d4399"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772107667,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix build"]],"content":"Signed commit: fix build","id":"2a8db19aff5126547a397f7daf9121f711a3d61efcced642b496687d9afc48dc","sig":"7e0558fac1764e185b3f52450f5a34805b04342bdb0821b4d459b1627d057d7e2af397b3263a8831e9be2e615556ef09094bce808c22f6049261273004da74bc"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772108817,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix build"]],"content":"Signed commit: fix build","id":"a37754536125d75a5c55f6af3b5521f89839e797ad1bffb69e3d313939cb7b65","sig":"6bcca1a025e4478ae330d3664dd2b9cff55f4bec82065ab2afb5bfb92031f7dde3264657dd892fe844396990117048b19247b0ef7423139f89d4cbf46b47f828"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772109639,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor the refactor"]],"content":"Signed commit: refactor the refactor","id":"62aafbdadfd37b20f1b16742a297e2b17d59dd3d6930e64e75d0d1b6a2f04bd6","sig":"050eaca1703b73443b51fd160932a2edfa04fc0a5efd3b5bb0a1e4c8b944caa60d444b2148c07b74f4ff4a589984fa524a7109a2a89c3eddf6c937b23b18c69b"} diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 41ae2f5..2a0cd5f 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -86,6 +86,19 @@ createBranch as createBranchService, deleteBranch as deleteBranchService } from './services/branch-operations.js'; + import { + loadTags as loadTagsService, + createTag as createTagService + } from './services/tag-operations.js'; + import { + loadReleases as loadReleasesService, + createRelease as createReleaseService + } from './services/release-operations.js'; + import { + loadCommitHistory as loadCommitHistoryService, + verifyCommit as verifyCommitService, + viewDiff as viewDiffService + } from './services/commit-operations.js'; // Consolidated state - all state variables in one object let state = $state(createRepoState()); @@ -3103,259 +3116,35 @@ } async function loadCommitHistory() { - state.loading.commits = true; - state.error = null; - try { - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/commits?branch=${state.git.currentBranch}&limit=50`, { - headers: buildApiHeaders() - }); - if (response.ok) { - const data = await response.json(); - // 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 state.git.commits in background (only for cloned repos) - if (state.clone.isCloned === true) { - state.git.commits.forEach(commit => { - 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; - } + await loadCommitHistoryService(state, { verifyCommit }); } async function verifyCommit(commitHash: string) { - 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 response = await fetch(`/api/repos/${state.npub}/${state.repo}/commits/${commitHash}/verify`, { - headers: buildApiHeaders() - }); - if (response.ok) { - const verification = await response.json(); - // 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); - } + await verifyCommitService(commitHash, state); } async function viewDiff(commitHash: string) { - // 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 response = await fetch(`/api/repos/${state.npub}/${state.repo}/diff?from=${parentHash}&to=${commitHash}`, { - headers: buildApiHeaders() - }); - if (response.ok) { - state.git.diffData = await response.json(); - state.git.showDiff = true; - } else { - // Handle 404 or other errors - const errorText = await response.text().catch(() => response.statusText); - if (response.status === 404) { - // 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: ${errorText || 'The commit may not exist in the repository'}`; - } - } else { - state.error = `Failed to load diff: ${errorText || response.statusText}`; - } - } - } catch (err) { - // Handle network errors - if (err instanceof TypeError && err.message.includes('NetworkError')) { - state.error = 'Network error: Unable to fetch diff. Please check your connection and try again.'; - } else { - state.error = err instanceof Error ? err.message : 'Failed to load diff'; - } - } finally { - state.loading.commits = false; - } + await viewDiffService(commitHash, state); } async function loadTags() { - if (state.repoNotFound) return; - try { - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/tags`, { - headers: buildApiHeaders() - }); - if (response.ok) { - state.git.tags = await response.json(); - // 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); - } + await loadTagsService(state, { loadTags }); } async function createTag() { - 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 { - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/tags`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...buildApiHeaders() - }, - body: JSON.stringify({ - tagName: state.forms.tag.name, - ref: state.forms.tag.ref, - message: state.forms.tag.message || undefined, - userPubkey: state.user.pubkey - }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || 'Failed to create tag'); - } - - state.openDialog = null; - state.forms.tag.name = ''; - state.forms.tag.message = ''; - await loadTags(); - alert('Tag created successfully!'); - } catch (err) { - state.error = err instanceof Error ? err.message : 'Failed to create tag'; - } finally { - state.saving = false; - } + await createTagService(state, { loadTags }); } async function loadReleases() { - if (state.repoNotFound) return; - state.loading.releases = true; - try { - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/releases`, { - headers: buildApiHeaders() - }); - if (response.ok) { - const data = await response.json(); - 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 state.releases:', err); - } finally { - state.loading.releases = false; - } + await loadReleasesService(state, { loadReleases }); } async function createRelease() { - 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 state.releases'); - return; - } - - state.creating.release = true; - state.error = null; - - try { - const response = await fetch(`/api/repos/${state.npub}/${state.repo}/releases`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...buildApiHeaders() - }, - body: JSON.stringify({ - 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 - }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || 'Failed to create release'); - } - - 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 loadReleases(); - // Reload state.git.tags to show release indicator - await loadTags(); - 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; - } + await createReleaseService(state, repoOwnerPubkeyDerived, { + loadReleases + }); + // Reload tags to show release indicator + await loadTags(); } async function performCodeSearch() { diff --git a/src/routes/repos/[npub]/[repo]/services/commit-operations.ts b/src/routes/repos/[npub]/[repo]/services/commit-operations.ts new file mode 100644 index 0000000..e504423 --- /dev/null +++ b/src/routes/repos/[npub]/[repo]/services/commit-operations.ts @@ -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; +} + +/** + * Load commit history + */ +export async function loadCommitHistory( + state: RepoState, + callbacks: CommitOperationsCallbacks +): Promise { + state.loading.commits = true; + state.error = null; + try { + const data = await apiRequest>(`/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 { + 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 { + // 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>(`/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; + } +} diff --git a/src/routes/repos/[npub]/[repo]/services/release-operations.ts b/src/routes/repos/[npub]/[repo]/services/release-operations.ts new file mode 100644 index 0000000..2080aaa --- /dev/null +++ b/src/routes/repos/[npub]/[repo]/services/release-operations.ts @@ -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; +} + +/** + * Load releases from the repository + */ +export async function loadReleases( + state: RepoState, + callbacks: ReleaseOperationsCallbacks +): Promise { + if (state.repoNotFound) return; + state.loading.releases = true; + try { + const data = await apiRequest>( + `/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 { + 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; + } +} diff --git a/src/routes/repos/[npub]/[repo]/services/tag-operations.ts b/src/routes/repos/[npub]/[repo]/services/tag-operations.ts new file mode 100644 index 0000000..141bc8a --- /dev/null +++ b/src/routes/repos/[npub]/[repo]/services/tag-operations.ts @@ -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; +} + +/** + * Load tags from the repository + */ +export async function loadTags( + state: RepoState, + callbacks: TagOperationsCallbacks +): Promise { + if (state.repoNotFound) return; + try { + const tags = await apiRequest>( + `/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 { + 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; + } +}