Browse Source
Nostr-Signature: 62aafbdadfd37b20f1b16742a297e2b17d59dd3d6930e64e75d0d1b6a2f04bd6 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 050eaca1703b73443b51fd160932a2edfa04fc0a5efd3b5bb0a1e4c8b944caa60d444b2148c07b74f4ff4a589984fa524a7109a2a89c3eddf6c937b23b18c69bmain
4 changed files with 388 additions and 336 deletions
@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
|
||||
/** |
||||
* Branch operations service |
||||
* Handles branch creation and deletion |
||||
*/ |
||||
|
||||
import type { NostrEvent } from '$lib/types/nostr.js'; |
||||
import type { RepoState } from '../stores/repo-state.js'; |
||||
import { apiPost, apiRequest } from '../utils/api-client.js'; |
||||
|
||||
interface BranchOperationsCallbacks { |
||||
loadBranches: () => Promise<void>; |
||||
} |
||||
|
||||
/** |
||||
* Create a new branch |
||||
*/ |
||||
export async function createBranch( |
||||
state: RepoState, |
||||
repoAnnouncement: NostrEvent | null | undefined, |
||||
callbacks: BranchOperationsCallbacks |
||||
): Promise<void> { |
||||
if (!state.forms.branch.name.trim()) { |
||||
alert('Please enter a branch name'); |
||||
return; |
||||
} |
||||
|
||||
state.saving = true; |
||||
state.error = null; |
||||
|
||||
try { |
||||
// If no branches exist, don't pass fromBranch (will use --orphan)
|
||||
// Otherwise, use the selected branch or current branch
|
||||
let fromBranch: string | undefined = state.forms.branch.from || state.git.currentBranch || undefined; |
||||
|
||||
// Include announcement if available (for empty repos)
|
||||
const requestBody: { branchName: string; fromBranch?: string; announcement?: NostrEvent } = { |
||||
branchName: state.forms.branch.name |
||||
}; |
||||
if (state.git.branches.length > 0 && fromBranch) { |
||||
requestBody.fromBranch = fromBranch; |
||||
} |
||||
// Pass announcement if available (especially useful for empty repos)
|
||||
if (repoAnnouncement) { |
||||
requestBody.announcement = repoAnnouncement; |
||||
} |
||||
|
||||
await apiPost(`/api/repos/${state.npub}/${state.repo}/branches`, requestBody); |
||||
|
||||
state.openDialog = null; |
||||
state.forms.branch.name = ''; |
||||
await callbacks.loadBranches(); |
||||
alert('Branch created successfully!'); |
||||
} catch (err) { |
||||
state.error = err instanceof Error ? err.message : 'Failed to create branch'; |
||||
} finally { |
||||
state.saving = false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Delete a branch |
||||
*/ |
||||
export async function deleteBranch( |
||||
branchName: string, |
||||
state: RepoState, |
||||
callbacks: BranchOperationsCallbacks |
||||
): Promise<void> { |
||||
if (!confirm(`Are you sure you want to delete the branch "${branchName}"?\n\nThis will permanently delete the branch from the repository. This action CANNOT be undone.\n\nClick OK to delete, or Cancel to abort.`)) { |
||||
return; |
||||
} |
||||
|
||||
if (!state.user.pubkey) { |
||||
alert('Please connect your NIP-07 extension'); |
||||
return; |
||||
} |
||||
|
||||
// Prevent deleting the current branch
|
||||
if (branchName === state.git.currentBranch) { |
||||
alert('Cannot delete the currently selected branch. Please switch to a different branch first.'); |
||||
return; |
||||
} |
||||
|
||||
state.saving = true; |
||||
state.error = null; |
||||
|
||||
try { |
||||
// Note: DELETE endpoint expects branchName in body, not query string
|
||||
await apiRequest(`/api/repos/${state.npub}/${state.repo}/branches`, { |
||||
method: 'DELETE', |
||||
body: JSON.stringify({ branchName }) |
||||
}); |
||||
|
||||
await callbacks.loadBranches(); |
||||
alert('Branch deleted successfully!'); |
||||
} catch (err) { |
||||
state.error = err instanceof Error ? err.message : 'Failed to delete branch'; |
||||
alert(state.error); |
||||
} finally { |
||||
state.saving = false; |
||||
} |
||||
} |
||||
@ -0,0 +1,255 @@
@@ -0,0 +1,255 @@
|
||||
/** |
||||
* File operations service |
||||
* Handles file saving, creating, and deleting |
||||
* Note: loadFile and loadFiles remain in component due to complex state dependencies |
||||
*/ |
||||
|
||||
import type { NostrEvent } from '$lib/types/nostr.js'; |
||||
import type { RepoState } from '../stores/repo-state.js'; |
||||
import { isNIP07Available, signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js'; |
||||
import { apiPost } from '../utils/api-client.js'; |
||||
|
||||
interface FileOperationsCallbacks { |
||||
getUserEmail: () => Promise<string>; |
||||
getUserName: () => Promise<string>; |
||||
loadFiles: (path: string) => Promise<void>; |
||||
loadFile?: (path: string) => Promise<void>; |
||||
} |
||||
|
||||
/** |
||||
* Save a file to the repository |
||||
*/ |
||||
export async function saveFile( |
||||
state: RepoState, |
||||
callbacks: FileOperationsCallbacks |
||||
): Promise<void> { |
||||
if (!state.files.currentFile || !state.forms.commit.message.trim()) { |
||||
alert('Please enter a commit message'); |
||||
return; |
||||
} |
||||
|
||||
if (!state.user.pubkey) { |
||||
alert('Please connect your NIP-07 extension to save files'); |
||||
return; |
||||
} |
||||
|
||||
if (!state.git.currentBranch || typeof state.git.currentBranch !== 'string') { |
||||
alert('Please select a branch before saving the file'); |
||||
return; |
||||
} |
||||
|
||||
state.saving = true; |
||||
state.error = null; |
||||
|
||||
try { |
||||
const authorEmail = await callbacks.getUserEmail(); |
||||
const authorName = await callbacks.getUserName(); |
||||
|
||||
// Sign commit with NIP-07 (client-side)
|
||||
let commitSignatureEvent: NostrEvent | null = null; |
||||
if (isNIP07Available()) { |
||||
try { |
||||
const { KIND } = await import('$lib/types/nostr.js'); |
||||
const timestamp = Math.floor(Date.now() / 1000); |
||||
const eventTemplate: Omit<NostrEvent, 'sig' | 'id'> = { |
||||
kind: KIND.COMMIT_SIGNATURE, |
||||
pubkey: '', |
||||
created_at: timestamp, |
||||
tags: [ |
||||
['author', authorName, authorEmail], |
||||
['message', state.forms.commit.message.trim()] |
||||
], |
||||
content: `Signed commit: ${state.forms.commit.message.trim()}` |
||||
}; |
||||
commitSignatureEvent = await signEventWithNIP07(eventTemplate); |
||||
} catch (err) { |
||||
console.warn('Failed to sign commit with NIP-07:', err); |
||||
} |
||||
} |
||||
|
||||
await apiPost(`/api/repos/${state.npub}/${state.repo}/file`, { |
||||
path: state.files.currentFile, |
||||
content: state.files.editedContent, |
||||
message: state.forms.commit.message.trim(), |
||||
authorName: authorName, |
||||
authorEmail: authorEmail, |
||||
branch: state.git.currentBranch, |
||||
userPubkey: state.user.pubkey, |
||||
commitSignatureEvent: commitSignatureEvent |
||||
}); |
||||
|
||||
if (callbacks.loadFile) { |
||||
await callbacks.loadFile(state.files.currentFile); |
||||
} |
||||
state.forms.commit.message = ''; |
||||
state.openDialog = null; |
||||
alert('File saved successfully!'); |
||||
} catch (err) { |
||||
state.error = err instanceof Error ? err.message : 'Failed to save file'; |
||||
console.error('Error saving file:', err); |
||||
} finally { |
||||
state.saving = false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Create a new file in the repository |
||||
*/ |
||||
export async function createFile( |
||||
state: RepoState, |
||||
callbacks: FileOperationsCallbacks |
||||
): Promise<void> { |
||||
if (!state.forms.file.fileName.trim()) { |
||||
alert('Please enter a file name'); |
||||
return; |
||||
} |
||||
|
||||
if (!state.user.pubkey) { |
||||
alert('Please connect your NIP-07 extension'); |
||||
return; |
||||
} |
||||
|
||||
if (!state.git.currentBranch || typeof state.git.currentBranch !== 'string') { |
||||
alert('Please select a branch before creating the file'); |
||||
return; |
||||
} |
||||
|
||||
state.saving = true; |
||||
state.error = null; |
||||
|
||||
try { |
||||
const authorEmail = await callbacks.getUserEmail(); |
||||
const authorName = await callbacks.getUserName(); |
||||
const filePath = state.files.currentPath ? `${state.files.currentPath}/${state.forms.file.fileName}` : state.forms.file.fileName; |
||||
const commitMsg = `Create ${state.forms.file.fileName}`; |
||||
|
||||
// Sign commit with NIP-07 (client-side)
|
||||
let commitSignatureEvent: NostrEvent | null = null; |
||||
if (isNIP07Available()) { |
||||
try { |
||||
const { KIND } = await import('$lib/types/nostr.js'); |
||||
const timestamp = Math.floor(Date.now() / 1000); |
||||
const eventTemplate: Omit<NostrEvent, 'sig' | 'id'> = { |
||||
kind: KIND.COMMIT_SIGNATURE, |
||||
pubkey: '', |
||||
created_at: timestamp, |
||||
tags: [ |
||||
['author', authorName, authorEmail], |
||||
['message', commitMsg] |
||||
], |
||||
content: `Signed commit: ${commitMsg}` |
||||
}; |
||||
commitSignatureEvent = await signEventWithNIP07(eventTemplate); |
||||
} catch (err) { |
||||
console.warn('Failed to sign commit with NIP-07:', err); |
||||
} |
||||
} |
||||
|
||||
await apiPost(`/api/repos/${state.npub}/${state.repo}/file`, { |
||||
path: filePath, |
||||
content: state.forms.file.content, |
||||
message: commitMsg, |
||||
authorName: authorName, |
||||
authorEmail: authorEmail, |
||||
branch: state.git.currentBranch, |
||||
action: 'create', |
||||
userPubkey: state.user.pubkey, |
||||
commitSignatureEvent: commitSignatureEvent |
||||
}); |
||||
|
||||
// Clear form
|
||||
state.forms.file.fileName = ''; |
||||
state.forms.file.content = ''; |
||||
state.openDialog = null; |
||||
|
||||
// Reload file list
|
||||
await callbacks.loadFiles(state.files.currentPath); |
||||
|
||||
alert('File created successfully!'); |
||||
} catch (err) { |
||||
state.error = err instanceof Error ? err.message : 'Failed to create file'; |
||||
console.error('Error creating file:', err); |
||||
} finally { |
||||
state.saving = false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Delete a file from the repository |
||||
*/ |
||||
export async function deleteFile( |
||||
filePath: string, |
||||
state: RepoState, |
||||
callbacks: FileOperationsCallbacks |
||||
): Promise<void> { |
||||
if (!confirm(`Are you sure you want to delete "${filePath}"?\n\nThis will permanently delete the file from the repository. This action cannot be undone.\n\nClick OK to delete, or Cancel to abort.`)) { |
||||
return; |
||||
} |
||||
|
||||
if (!state.user.pubkey) { |
||||
alert('Please connect your NIP-07 extension'); |
||||
return; |
||||
} |
||||
|
||||
if (!state.git.currentBranch || typeof state.git.currentBranch !== 'string') { |
||||
alert('Please select a branch before deleting the file'); |
||||
return; |
||||
} |
||||
|
||||
state.saving = true; |
||||
state.error = null; |
||||
|
||||
try { |
||||
const authorEmail = await callbacks.getUserEmail(); |
||||
const authorName = await callbacks.getUserName(); |
||||
const commitMsg = `Delete ${filePath}`; |
||||
|
||||
// Sign commit with NIP-07 (client-side)
|
||||
let commitSignatureEvent: NostrEvent | null = null; |
||||
if (isNIP07Available()) { |
||||
try { |
||||
const { KIND } = await import('$lib/types/nostr.js'); |
||||
const timestamp = Math.floor(Date.now() / 1000); |
||||
const eventTemplate: Omit<NostrEvent, 'sig' | 'id'> = { |
||||
kind: KIND.COMMIT_SIGNATURE, |
||||
pubkey: '', |
||||
created_at: timestamp, |
||||
tags: [ |
||||
['author', authorName, authorEmail], |
||||
['message', commitMsg] |
||||
], |
||||
content: `Signed commit: ${commitMsg}` |
||||
}; |
||||
commitSignatureEvent = await signEventWithNIP07(eventTemplate); |
||||
} catch (err) { |
||||
console.warn('Failed to sign commit with NIP-07:', err); |
||||
} |
||||
} |
||||
|
||||
await apiPost(`/api/repos/${state.npub}/${state.repo}/file`, { |
||||
path: filePath, |
||||
message: commitMsg, |
||||
authorName: authorName, |
||||
authorEmail: authorEmail, |
||||
branch: state.git.currentBranch, |
||||
action: 'delete', |
||||
userPubkey: state.user.pubkey, |
||||
commitSignatureEvent: commitSignatureEvent |
||||
}); |
||||
|
||||
// Clear current file if it was deleted
|
||||
if (state.files.currentFile === filePath) { |
||||
state.files.currentFile = null; |
||||
} |
||||
|
||||
// Reload file list
|
||||
await callbacks.loadFiles(state.files.currentPath); |
||||
|
||||
alert('File deleted successfully!'); |
||||
} catch (err) { |
||||
state.error = err instanceof Error ? err.message : 'Failed to delete file'; |
||||
console.error('Error deleting file:', err); |
||||
} finally { |
||||
state.saving = false; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue