Browse Source

fix creating new branch

Nostr-Signature: bc6c623532064f9b2db08fa41bbc6c5ff42419415ca7e1ecb1162a884face2eb 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc ad1152e2848755e1afa7d9350716fa6bb709698a5036e21efa61b3ac755d334155f02a0622ad49f6dc060d523f4f886eb2acc8c80356a426b0d8ba454fdcb8ee
main
Silberengel 3 weeks ago
parent
commit
ce6c40c0c4
  1. 1
      nostr/commit-signatures.jsonl
  2. 5
      src/lib/components/RepoHeaderEnhanced.svelte
  3. 250
      src/lib/services/git/file-manager.ts
  4. 21
      src/routes/api/repos/[npub]/[repo]/branches/+server.ts
  5. 77
      src/routes/repos/[npub]/[repo]/+page.svelte

1
nostr/commit-signatures.jsonl

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

5
src/lib/components/RepoHeaderEnhanced.svelte

@ -351,17 +351,18 @@ @@ -351,17 +351,18 @@
</div>
{/if}
{#if branches.length > 0 && currentBranch}
{#if currentBranch}
<div class="repo-branch">
<button
class="branch-button"
onclick={() => showBranchMenu = !showBranchMenu}
aria-expanded={showBranchMenu}
disabled={branches.length === 0}
>
<img src="/icons/git-branch.svg" alt="" class="icon" />
{currentBranch}
</button>
{#if showBranchMenu}
{#if showBranchMenu && branches.length > 0}
<div class="branch-menu">
{#each branches as branch}
{@const branchName = typeof branch === 'string' ? branch : branch.name}

250
src/lib/services/git/file-manager.ts

@ -139,6 +139,112 @@ export class FileManager { @@ -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<void>((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 <noreply@gitrepublic.com>'
});
// 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<void>((resolve, reject) => {
const gitProcess = spawn('git', ['worktree', 'add', worktreePath, branch], {
@ -156,7 +262,7 @@ export class FileManager { @@ -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<string> => {
try {
@ -199,9 +305,33 @@ export class FileManager { @@ -199,9 +305,33 @@ export class FileManager {
branchProcess.on('close', (branchCode) => {
if (branchCode === 0) {
resolveBranch();
} else {
// 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}`));
}
}
});
branchProcess.on('error', rejectBranch);
@ -222,9 +352,32 @@ export class FileManager { @@ -222,9 +352,32 @@ export class FileManager {
gitProcess2.on('close', (code2) => {
if (code2 === 0) {
resolve2();
} else {
// 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}`));
}
}
});
gitProcess2.on('error', reject2);
@ -486,7 +639,41 @@ export class FileManager { @@ -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 { @@ -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 { @@ -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 { @@ -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 <noreply@gitrepublic.com>'
});
// 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 {

21
src/routes/api/repos/[npub]/[repo]/branches/+server.ts

@ -10,7 +10,7 @@ import { createRepoGetHandler, createRepoPostHandler } from '$lib/utils/api-hand @@ -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( @@ -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( @@ -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(

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

@ -2698,6 +2698,16 @@ @@ -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 @@ @@ -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 @@ @@ -4883,6 +4896,25 @@
<div class="editor-header">
<span class="file-path">{currentFile}</span>
<div class="editor-actions">
{#if branches.length > 0 && isMaintainer}
<select
bind:value={currentBranch}
class="branch-selector"
disabled={saving || needsClone}
title="Select branch"
onchange={() => {
// Use the existing handleBranchChange function
handleBranchChangeDirect(currentBranch || '');
}}
>
{#each branches as branch}
{@const branchName = typeof branch === 'string' ? branch : branch.name}
<option value={branchName}>{branchName}{#if branchName === defaultBranch} (default){/if}</option>
{/each}
</select>
{:else if currentBranch && isMaintainer}
<span class="branch-display" title="Current branch">{currentBranch}</span>
{/if}
{#if hasChanges}
<span class="unsaved-indicator">● Unsaved changes</span>
{/if}
@ -5485,6 +5517,22 @@ @@ -5485,6 +5517,22 @@
onclick={(e) => e.stopPropagation()}
>
<h3>Create New File</h3>
{#if branches.length > 0}
<label>
Branch:
<select bind:value={currentBranch} disabled={saving}>
{#each branches as branch}
{@const branchName = typeof branch === 'string' ? branch : branch.name}
<option value={branchName}>{branchName}{#if branchName === defaultBranch} (default){/if}</option>
{/each}
</select>
</label>
{:else if currentBranch}
<label>
Branch:
<input type="text" value={currentBranch} disabled />
</label>
{/if}
<label>
File Name:
<input type="text" bind:value={newFileName} placeholder="filename.md" />
@ -5497,9 +5545,9 @@ @@ -5497,9 +5545,9 @@
<button onclick={() => showCreateFileDialog = false} class="cancel-button">Cancel</button>
<button
onclick={createFile}
disabled={!newFileName.trim() || saving || needsClone}
disabled={!newFileName.trim() || saving || needsClone || !currentBranch}
class="save-button"
title={needsClone ? cloneTooltip : ''}
title={needsClone ? cloneTooltip : (!currentBranch ? 'Please select a branch' : '')}
>
{saving ? 'Creating...' : 'Create'}
</button>
@ -5535,11 +5583,14 @@ @@ -5535,11 +5583,14 @@
From Branch:
<select bind:value={newBranchFrom}>
{#if branches.length === 0}
<option value={null}>No branches - will create initial branch ({defaultBranchName})</option>
<option value={null}>No branches - will create initial branch</option>
{:else}
{#each branches as branch}
{@const branchName = typeof branch === 'string' ? branch : (branch as { name: string }).name}
{@const isDefaultBranch = branchName === defaultBranchName}
{#if !isDefaultBranch}
<option value={branchName}>{branchName}</option>
{/if}
{/each}
{/if}
</select>
@ -5841,6 +5892,22 @@ @@ -5841,6 +5892,22 @@
onclick={(e) => e.stopPropagation()}
>
<h3>Commit Changes</h3>
{#if branches.length > 0}
<label>
Branch:
<select bind:value={currentBranch} disabled={saving}>
{#each branches as branch}
{@const branchName = typeof branch === 'string' ? branch : branch.name}
<option value={branchName}>{branchName}{#if branchName === defaultBranch} (default){/if}</option>
{/each}
</select>
</label>
{:else if currentBranch}
<label>
Branch:
<input type="text" value={currentBranch} disabled />
</label>
{/if}
<label>
Commit Message:
<textarea
@ -5853,9 +5920,9 @@ @@ -5853,9 +5920,9 @@
<button onclick={() => showCommitDialog = false} class="cancel-button">Cancel</button>
<button
onclick={saveFile}
disabled={!commitMessage.trim() || saving || needsClone}
disabled={!commitMessage.trim() || saving || needsClone || !currentBranch}
class="save-button"
title={needsClone ? cloneTooltip : ''}
title={needsClone ? cloneTooltip : (!currentBranch ? 'Please select a branch' : '')}
>
{saving ? 'Saving...' : 'Commit & Save'}
</button>

Loading…
Cancel
Save