From ce6c40c0c448a4bf8e52e12afc0b7b375b6179a3 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 22 Feb 2026 11:23:31 +0100 Subject: [PATCH] fix creating new branch Nostr-Signature: bc6c623532064f9b2db08fa41bbc6c5ff42419415ca7e1ecb1162a884face2eb 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc ad1152e2848755e1afa7d9350716fa6bb709698a5036e21efa61b3ac755d334155f02a0622ad49f6dc060d523f4f886eb2acc8c80356a426b0d8ba454fdcb8ee --- nostr/commit-signatures.jsonl | 1 + src/lib/components/RepoHeaderEnhanced.svelte | 5 +- src/lib/services/git/file-manager.ts | 254 +++++++++++++++++- .../repos/[npub]/[repo]/branches/+server.ts | 21 +- src/routes/repos/[npub]/[repo]/+page.svelte | 79 +++++- 5 files changed, 345 insertions(+), 15 deletions(-) diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 61b32e5..1cd1b04 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -54,3 +54,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771750596,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix git folders"]],"content":"Signed commit: fix git folders","id":"3d2475034fdfa5eea36e5caad946460b034a1e4e16b6ba6e3f7fb9b6e1b0a31f","sig":"3eb6e3300081a53434e0f692f0c46618369089bb25047a83138ef3ffd485f749cf817b480f5c8ff0458bb846d04654ba2730ba7d42272739af18a13e8dcb4ed4"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771753256,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","markup and csv previews in file viewer\ncorrect image view\ncorrect syntax view\nadd copy, raw, and download buttons"]],"content":"Signed commit: markup and csv previews in file viewer\ncorrect image view\ncorrect syntax view\nadd copy, raw, and download buttons","id":"40e64c0a716e0ff594b736db14021e43583d5ff0918c1ec0c4fe2c07ddbdbc73","sig":"bb3a50267214a005104853e9b78dd94e4980024146978baef8612ef0400024032dd620749621f832ee9f0458e582084f12ed9c85a40c306f5bbc92e925198a97"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771754094,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"146ea5bbc462c4f0188ec4a35a248c2cf518af7088714a4c1ce8e6e35f524e2a","sig":"dfc5d8d9a2f35e1898404d096f6e3e334885cdb0076caab0f3ea3efd1236e53d4172ed2b9ec16cff80ff364898c287ddb400b7a52cb65a3aedc05bb9df0f7ace"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771754488,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix menu responsivenes on repo-header"]],"content":"Signed commit: fix menu responsivenes on repo-header","id":"4dd8101d8edc9431df49d9fe23b7e1e545e11ef32b024b44f871bb962fb8ad4c","sig":"dbcfbfafe02495971b3f3d18466ecf1d894e4001a41e4038d17fd78bb65124de347017273a0a437c397a79ff8226ec6b0718436193e474ef8969392df027fa34"} diff --git a/src/lib/components/RepoHeaderEnhanced.svelte b/src/lib/components/RepoHeaderEnhanced.svelte index 2ad5965..333c77c 100644 --- a/src/lib/components/RepoHeaderEnhanced.svelte +++ b/src/lib/components/RepoHeaderEnhanced.svelte @@ -351,17 +351,18 @@ {/if} - {#if branches.length > 0 && currentBranch} + {#if currentBranch}
- {#if showBranchMenu} + {#if showBranchMenu && branches.length > 0}
{#each branches as branch} {@const branchName = typeof branch === 'string' ? branch : branch.name} diff --git a/src/lib/services/git/file-manager.ts b/src/lib/services/git/file-manager.ts index 24ce032..6ebd0cd 100644 --- a/src/lib/services/git/file-manager.ts +++ b/src/lib/services/git/file-manager.ts @@ -139,6 +139,112 @@ export class FileManager { // Create new worktree try { + // First, check if the branch exists and has commits + let branchExists = false; + let branchHasCommits = false; + try { + // Check if branch exists + const branchList = await git.branch(['-a']); + const branchNames = branchList.all.map(b => b.replace(/^remotes\/origin\//, '').replace(/^remotes\//, '')); + branchExists = branchNames.includes(branch); + + if (branchExists) { + // Check if branch has commits by trying to get the latest commit + try { + const commitHash = await git.raw(['rev-parse', `refs/heads/${branch}`]); + branchHasCommits = !!(commitHash && commitHash.trim().length > 0); + } catch { + // Branch exists but has no commits (orphan branch) + branchHasCommits = false; + } + } + } catch (err) { + logger.debug({ error: err, branch }, 'Could not check branch status, will try to create worktree'); + } + + // If branch exists but has no commits, create an initial empty commit first + if (branchExists && !branchHasCommits) { + logger.debug({ branch }, 'Branch exists but has no commits, creating initial empty commit'); + try { + // Fetch repo announcement to use as commit message + let commitMessage = 'Initial commit'; + try { + const { NostrClient } = await import('../nostr/nostr-client.js'); + const { DEFAULT_NOSTR_RELAYS } = await import('../../config.js'); + const { fetchRepoAnnouncementsWithCache, findRepoAnnouncement } = await import('../../utils/nostr-utils.js'); + const { eventCache } = await import('../nostr/event-cache.js'); + const { nip19 } = await import('nostr-tools'); + const { requireNpubHex } = await import('../../utils/npub-utils.js'); + + const repoOwnerPubkey = requireNpubHex(npub); + const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); + const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache); + const announcement = findRepoAnnouncement(allEvents, repoName); + + if (announcement) { + // Format announcement as commit message + const name = announcement.tags.find((t: string[]) => t[0] === 'name')?.[1] || repoName; + const description = announcement.tags.find((t: string[]) => t[0] === 'description')?.[1] || ''; + commitMessage = `Repository announcement: ${name}${description ? '\n\n' + description : ''}\n\nEvent ID: ${announcement.id}`; + logger.debug({ branch, announcementId: announcement.id }, 'Using repo announcement as initial commit message'); + } + } catch (announcementErr) { + logger.debug({ error: announcementErr, branch }, 'Failed to fetch announcement, using default commit message'); + } + + // Create a temporary worktree with a temp branch name to make the initial commit + const tempBranchName = `.temp-init-${Date.now()}`; + const tempWorktreePath = resolve(join(this.repoRoot, npub, `${repoName}.worktrees`, tempBranchName)); + const { mkdir } = await import('fs/promises'); + await mkdir(dirname(tempWorktreePath), { recursive: true }); + + // Create orphan worktree with temp branch + await new Promise((resolve, reject) => { + const orphanProcess = spawn('git', ['worktree', 'add', '--orphan', tempBranchName, tempWorktreePath], { + cwd: repoPath, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let orphanStderr = ''; + orphanProcess.stderr.on('data', (chunk: Buffer) => { + orphanStderr += chunk.toString(); + }); + + orphanProcess.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Failed to create orphan worktree: ${orphanStderr}`)); + } + }); + + orphanProcess.on('error', reject); + }); + + // Create initial empty commit in temp branch with announcement as message + const tempGit = simpleGit(tempWorktreePath); + await tempGit.commit(commitMessage, ['--allow-empty'], { + '--author': 'GitRepublic ' + }); + + // Get the commit hash + const commitHash = await tempGit.revparse(['HEAD']); + + // Update the actual branch to point to this commit + await git.raw(['update-ref', `refs/heads/${branch}`, commitHash.trim()]); + + // Remove temporary worktree and temp branch + await this.removeWorktree(repoPath, tempWorktreePath); + await git.raw(['branch', '-D', tempBranchName]).catch(() => { + // Ignore if branch deletion fails + }); + + logger.debug({ branch, commitHash }, 'Created initial empty commit on orphan branch with announcement'); + } catch (err) { + logger.warn({ error: err, branch }, 'Failed to create initial commit, will try normal worktree creation'); + } + } + // Use spawn for worktree add (safer than exec) await new Promise((resolve, reject) => { const gitProcess = spawn('git', ['worktree', 'add', worktreePath, branch], { @@ -156,7 +262,7 @@ export class FileManager { resolve(); } else { // If branch doesn't exist, create it first using git branch (works on bare repos) - if (stderr.includes('fatal: invalid reference') || stderr.includes('fatal: not a valid object name')) { + if (stderr.includes('fatal: invalid reference') || stderr.includes('fatal: not a valid object name') || stderr.includes('Ungültige Referenz')) { // First, try to find a source branch (HEAD, main, or master) const findSourceBranch = async (): Promise => { try { @@ -200,7 +306,31 @@ export class FileManager { if (branchCode === 0) { resolveBranch(); } else { - rejectBranch(new Error(`Failed to create branch: ${branchStderr}`)); + // If creating branch from source fails, try creating orphan branch + if (branchStderr.includes('fatal: invalid reference') || branchStderr.includes('Ungültige Referenz')) { + // Create orphan branch instead + const orphanProcess = spawn('git', ['branch', branch], { + cwd: repoPath, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let orphanStderr = ''; + orphanProcess.stderr.on('data', (chunk: Buffer) => { + orphanStderr += chunk.toString(); + }); + + orphanProcess.on('close', (orphanCode) => { + if (orphanCode === 0) { + resolveBranch(); + } else { + rejectBranch(new Error(`Failed to create orphan branch: ${orphanStderr}`)); + } + }); + + orphanProcess.on('error', rejectBranch); + } else { + rejectBranch(new Error(`Failed to create branch: ${branchStderr}`)); + } } }); @@ -223,7 +353,30 @@ export class FileManager { if (code2 === 0) { resolve2(); } else { - reject2(new Error(`Failed to create worktree after creating branch: ${retryStderr}`)); + // If still failing, try with --orphan + if (retryStderr.includes('fatal: invalid reference') || retryStderr.includes('Ungültige Referenz')) { + const orphanWorktreeProcess = spawn('git', ['worktree', 'add', '--orphan', branch, worktreePath], { + cwd: repoPath, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let orphanWorktreeStderr = ''; + orphanWorktreeProcess.stderr.on('data', (chunk: Buffer) => { + orphanWorktreeStderr += chunk.toString(); + }); + + orphanWorktreeProcess.on('close', (orphanWorktreeCode) => { + if (orphanWorktreeCode === 0) { + resolve2(); + } else { + reject2(new Error(`Failed to create orphan worktree: ${orphanWorktreeStderr}`)); + } + }); + + orphanWorktreeProcess.on('error', reject2); + } else { + reject2(new Error(`Failed to create worktree after creating branch: ${retryStderr}`)); + } } }); @@ -486,7 +639,41 @@ export class FileManager { // Note: git ls-tree returns paths relative to repo root, not relative to the specified path const gitPath = path ? (path.endsWith('/') ? path : `${path}/`) : '.'; logger.debug({ npub, repoName, path, ref, gitPath }, '[FileManager] Calling git ls-tree'); - const tree = await git.raw(['ls-tree', '-l', ref, gitPath]); + + let tree: string; + try { + tree = await git.raw(['ls-tree', '-l', ref, gitPath]); + } catch (lsTreeError) { + // Handle empty branches (orphan branches with no commits) + // git ls-tree will fail with "fatal: not a valid object name" or similar + const errorMsg = lsTreeError instanceof Error ? lsTreeError.message : String(lsTreeError); + const errorStr = String(lsTreeError).toLowerCase(); + const errorMsgLower = errorMsg.toLowerCase(); + + // Check for various error patterns that indicate empty branch/no commits + const isEmptyBranchError = + errorMsgLower.includes('not a valid object') || + errorMsgLower.includes('not found') || + errorMsgLower.includes('bad revision') || + errorMsgLower.includes('ambiguous argument') || + errorStr.includes('not a valid object') || + errorStr.includes('not found') || + errorStr.includes('bad revision') || + errorStr.includes('ambiguous argument') || + errorMsgLower.includes('fatal:') && (errorMsgLower.includes('master') || errorMsgLower.includes('refs/heads')); + + if (isEmptyBranchError) { + logger.debug({ npub, repoName, path, ref, gitPath, error: errorMsg, errorStr }, '[FileManager] Branch has no commits, returning empty list'); + const emptyResult: FileEntry[] = []; + // Cache empty result for shorter time (30 seconds) + repoCache.set(cacheKey, emptyResult, 30 * 1000); + return emptyResult; + } + // Log the error for debugging + logger.error({ npub, repoName, path, ref, gitPath, error: lsTreeError, errorMsg, errorStr }, '[FileManager] Unexpected error from git ls-tree'); + // Re-throw if it's a different error + throw lsTreeError; + } if (!tree || !tree.trim()) { const emptyResult: FileEntry[] = []; @@ -633,7 +820,31 @@ export class FileManager { return sortedEntries; } catch (error) { - logger.error({ error, repoPath, ref }, 'Error listing files'); + // Check if this is an empty branch error that wasn't caught earlier + const errorMsg = error instanceof Error ? error.message : String(error); + const errorStr = String(error).toLowerCase(); + const errorMsgLower = errorMsg.toLowerCase(); + + const isEmptyBranchError = + errorMsgLower.includes('not a valid object') || + errorMsgLower.includes('not found') || + errorMsgLower.includes('bad revision') || + errorMsgLower.includes('ambiguous argument') || + errorStr.includes('not a valid object') || + errorStr.includes('not found') || + errorStr.includes('bad revision') || + errorStr.includes('ambiguous argument') || + (errorMsgLower.includes('fatal:') && (errorMsgLower.includes('master') || errorMsgLower.includes('refs/heads'))); + + if (isEmptyBranchError) { + logger.debug({ npub, repoName, path, ref, error: errorMsg, errorStr }, '[FileManager] Branch has no commits (caught in outer catch), returning empty list'); + const emptyResult: FileEntry[] = []; + // Cache empty result for shorter time (30 seconds) + repoCache.set(cacheKey, emptyResult, 30 * 1000); + return emptyResult; + } + + logger.error({ error, repoPath, ref, errorMsg, errorStr }, 'Error listing files'); throw new Error(`Failed to list files: ${error instanceof Error ? error.message : String(error)}`); } } @@ -1382,6 +1593,31 @@ export class FileManager { // If no branches exist, create an orphan branch (branch with no parent) if (!hasBranches) { + // Fetch repo announcement to use as initial commit message + let commitMessage = 'Initial commit'; + try { + const { NostrClient } = await import('../nostr/nostr-client.js'); + const { DEFAULT_NOSTR_RELAYS } = await import('../../config.js'); + const { fetchRepoAnnouncementsWithCache, findRepoAnnouncement } = await import('../../utils/nostr-utils.js'); + const { eventCache } = await import('../nostr/event-cache.js'); + const { requireNpubHex } = await import('../../utils/npub-utils.js'); + + const repoOwnerPubkey = requireNpubHex(npub); + const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); + const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache); + const announcement = findRepoAnnouncement(allEvents, repoName); + + if (announcement) { + // Format announcement as commit message + const name = announcement.tags.find((t: string[]) => t[0] === 'name')?.[1] || repoName; + const description = announcement.tags.find((t: string[]) => t[0] === 'description')?.[1] || ''; + commitMessage = `Repository announcement: ${name}${description ? '\n\n' + description : ''}\n\nEvent ID: ${announcement.id}`; + logger.debug({ branchName, announcementId: announcement.id }, 'Using repo announcement as initial commit message'); + } + } catch (announcementErr) { + logger.debug({ error: announcementErr, branchName }, 'Failed to fetch announcement, using default commit message'); + } + // Create worktree for the new branch directly (orphan branch) const worktreeRoot = join(this.repoRoot, npub, `${repoName}.worktrees`); const worktreePath = resolve(join(worktreeRoot, branchName)); @@ -1403,9 +1639,17 @@ export class FileManager { // Create worktree with orphan branch await git.raw(['worktree', 'add', worktreePath, '--orphan', branchName]); + // Create initial empty commit with announcement as message + const workGit: SimpleGit = simpleGit(worktreePath); + await workGit.commit(commitMessage, ['--allow-empty'], { + '--author': 'GitRepublic ' + }); + // Set the default branch to the new branch in the bare repo await git.raw(['symbolic-ref', 'HEAD', `refs/heads/${branchName}`]); + logger.debug({ branchName }, 'Created orphan branch with initial commit'); + // Clean up worktree await this.removeWorktree(repoPath, worktreePath); } else { diff --git a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts index 3997e0e..f17f7a4 100644 --- a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts @@ -10,7 +10,7 @@ import { createRepoGetHandler, createRepoPostHandler } from '$lib/utils/api-hand import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js'; import { handleValidationError, handleApiError, handleNotFoundError } from '$lib/utils/error-handler.js'; import { KIND } from '$lib/types/nostr.js'; -import { join } from 'path'; +import { join, dirname } from 'path'; import { existsSync } from 'fs'; import { repoCache, RepoCache } from '$lib/services/git/repo-cache.js'; import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } from '$lib/config.js'; @@ -132,6 +132,23 @@ export const POST: RequestHandler = createRepoPostHandler( throw handleValidationError('Missing branchName parameter', { operation: 'createBranch', npub: context.npub, repo: context.repo }); } + const repoPath = join(repoRoot, context.npub, `${context.repo}.git`); + const repoExists = existsSync(repoPath); + + // Create repo if it doesn't exist + if (!repoExists) { + logger.info({ npub: context.npub, repo: context.repo }, 'Creating new empty repository for branch creation'); + const { mkdir } = await import('fs/promises'); + const repoDir = dirname(repoPath); + await mkdir(repoDir, { recursive: true }); + + // Initialize bare repository + const simpleGit = (await import('simple-git')).default; + const git = simpleGit(); + await git.init(['--bare', repoPath]); + logger.info({ npub: context.npub, repo: context.repo }, 'Empty repository created successfully'); + } + // Get default branch if fromBranch not provided // If repo has no branches, use 'master' as default let sourceBranch = fromBranch; @@ -148,7 +165,7 @@ export const POST: RequestHandler = createRepoPostHandler( await fileManager.createBranch(context.npub, context.repo, branchName, sourceBranch); return json({ success: true, message: 'Branch created successfully' }); }, - { operation: 'createBranch' } + { operation: 'createBranch', requireRepoExists: false } // Allow creating branches in empty repos ); export const DELETE: RequestHandler = createRepoPostHandler( diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 28145d9..1958979 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -2698,6 +2698,16 @@ !branchNames.includes(currentBranch)) { currentBranch = defaultBranch; } + } else { + // No branches loaded yet or empty repo - set currentBranch from settings if not set + if (!currentBranch) { + try { + const settings = await settingsStore.getSettings(); + currentBranch = settings.defaultBranch || 'master'; + } catch { + currentBranch = 'master'; + } + } } } else if (response.status === 404) { // Repository not provisioned yet - set error message and flag @@ -4285,6 +4295,9 @@ } catch { defaultBranchName = 'master'; } + // Preset the default branch name in the input field + newBranchName = defaultBranchName; + newBranchFrom = null; // Reset from branch selection showCreateBranchDialog = true; }} onSettings={() => goto(`/signup?npub=${npub}&repo=${repo}`)} @@ -4883,6 +4896,25 @@
{currentFile}
+ {#if branches.length > 0 && isMaintainer} + + {:else if currentBranch && isMaintainer} + {currentBranch} + {/if} {#if hasChanges} ● Unsaved changes {/if} @@ -5485,6 +5517,22 @@ onclick={(e) => e.stopPropagation()} >

Create New File

+ {#if branches.length > 0} + + {:else if currentBranch} + + {/if}