Browse Source

refactor 4

Nostr-Signature: d330d1a096e5f3951e8b2a66160a23c5ac28aa94313ecd0948c7e50baa60bdbb 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc febf4088cca3f7223f55ab300ed7fdb7b333c03d2534b05721dfaf9d9284f4599b385ba54379890fa6b846aed02d656a5e45429a6dd571dddbb997be6d8159b2
main
Silberengel 2 weeks ago
parent
commit
8984be75c1
  1. 1
      nostr/commit-signatures.jsonl
  2. 255
      src/routes/repos/[npub]/[repo]/+page.svelte
  3. 145
      src/routes/repos/[npub]/[repo]/services/commit-operations.ts
  4. 93
      src/routes/repos/[npub]/[repo]/services/release-operations.ts
  5. 73
      src/routes/repos/[npub]/[repo]/services/tag-operations.ts

1
nostr/commit-signatures.jsonl

@ -96,3 +96,4 @@ @@ -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"}

255
src/routes/repos/[npub]/[repo]/+page.svelte

@ -86,6 +86,19 @@ @@ -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 @@ @@ -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
})
await createReleaseService(state, repoOwnerPubkeyDerived, {
loadReleases
});
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
// Reload 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;
}
}
async function performCodeSearch() {

145
src/routes/repos/[npub]/[repo]/services/commit-operations.ts

@ -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;
}
}

93
src/routes/repos/[npub]/[repo]/services/release-operations.ts

@ -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;
}
}

73
src/routes/repos/[npub]/[repo]/services/tag-operations.ts

@ -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…
Cancel
Save