Browse Source
Nostr-Signature: 62aafbdadfd37b20f1b16742a297e2b17d59dd3d6930e64e75d0d1b6a2f04bd6 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 050eaca1703b73443b51fd160932a2edfa04fc0a5efd3b5bb0a1e4c8b944caa60d444b2148c07b74f4ff4a589984fa524a7109a2a89c3eddf6c937b23b18c69bmain
4 changed files with 388 additions and 336 deletions
@ -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 @@ |
|||||||
|
/** |
||||||
|
* 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