@ -1,15 +1,14 @@
@@ -1,15 +1,14 @@
/ * *
* API endpoint for getting and creating repository branches
* API endpoint for repository branches
* Handles GET ( list ) , POST ( create ) , and DELETE operations
* /
import { json , error } from '@sveltejs/kit' ;
// @ts-ignore - SvelteKit generates this type
import { json } from '@sveltejs/kit' ;
import type { RequestHandler } from './$types' ;
import { fileManager , repoManager , nostrClient } from '$lib/services/service-registry.js' ;
import { fileManager , repoManager , nostrClient , maintainerService } from '$lib/services/service-registry.js' ;
import { createRepoGetHandler , createRepoPostHandler } from '$lib/utils/api-handlers.js' ;
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 { handleValidationError , handleApiError , handleNotFoundError , handleAuthError , handleAuthorizationError } from '$lib/utils/error-handler.js' ;
import { join , dirname , resolve } from 'path' ;
import { existsSync , accessSync , constants } from 'fs' ;
import { repoCache , RepoCache } from '$lib/services/git/repo-cache.js' ;
@ -19,16 +18,22 @@ import { eventCache } from '$lib/services/nostr/event-cache.js';
@@ -19,16 +18,22 @@ import { eventCache } from '$lib/services/nostr/event-cache.js';
import { fetchRepoAnnouncementsWithCache , findRepoAnnouncement } from '$lib/utils/nostr-utils.js' ;
import { isGraspUrl } from '$lib/services/git/api-repo-fetcher.js' ;
import logger from '$lib/services/logger.js' ;
import simpleGit from 'simple-git' ;
// Resolve GIT_REPO_ROOT to absolute path
const repoRootEnv = typeof process !== 'undefined' && process . env ? . GIT_REPO_ROOT
? process . env . GIT_REPO_ROOT
: '/repos' ;
const repoRoot = resolve ( repoRootEnv ) ;
/ * *
* Check if a directory exists and is writable
* Provides helpful error messages for container environments
* /
function checkDirectoryWritable ( dirPath : string , description : string ) : void {
if ( ! existsSync ( dirPath ) ) {
const isContainer = existsSync ( '/.dockerenv' ) || process . env . DOCKER_CONTAINER === 'true' ;
const errorMsg = isContainer
? ` ${ description } does not exist at ${ dirPath } . In Docker, ensure the volume is mounted correctly and the directory exists on the host. Check docker-compose.yml volumes section . `
? ` ${ description } does not exist at ${ dirPath } . In Docker, ensure the volume is mounted correctly. `
: ` ${ description } does not exist at ${ dirPath } ` ;
throw new Error ( errorMsg ) ;
}
@ -38,41 +43,37 @@ function checkDirectoryWritable(dirPath: string, description: string): void {
@@ -38,41 +43,37 @@ function checkDirectoryWritable(dirPath: string, description: string): void {
} catch ( accessErr ) {
const isContainer = existsSync ( '/.dockerenv' ) || process . env . DOCKER_CONTAINER === 'true' ;
const errorMsg = isContainer
? ` ${ description } at ${ dirPath } is not writable. In Docker, check that the volume mount has correct permissions. The container runs as user 'gitrepublic' (UID 1001). Ensure the host directory is writable by this user or adjust ownership: chown -R 1001:1001 ./repos `
? ` ${ description } at ${ dirPath } is not writable. Check volume mount permissions. `
: ` ${ description } at ${ dirPath } is not writable ` ;
throw new Error ( errorMsg ) ;
}
}
// Resolve GIT_REPO_ROOT to absolute path (handles both relative and absolute paths)
const repoRootEnv = typeof process !== 'undefined' && process . env ? . GIT_REPO_ROOT
? process . env . GIT_REPO_ROOT
: '/repos' ;
const repoRoot = resolve ( repoRootEnv ) ;
/ * *
* GET : List branches in a repository
* /
export const GET : RequestHandler = createRepoGetHandler (
async ( context : RepoRequestContext , event : RequestEvent ) = > {
const repoPath = join ( repoRoot , context . npub , ` ${ context . repo } .git ` ) ;
const skipApiFallback = event . url . searchParams . get ( 'skipApiFallback' ) === 'true' ;
// If repo doesn't exist, try to fetch it on-demand (unless skipApiFallback is true)
// If repo doesn't exist, try API fallback (unless skipApiFallback is true)
if ( ! existsSync ( repoPath ) ) {
// If skipApiFallback is true, return 404 immediately to indicate repo is not cloned
if ( skipApiFallback ) {
throw handleNotFoundError (
'Repository is not cloned locally' ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
}
try {
// Fetch repository announcement (case-insensitive) with caching
// Fetch repository announcement
let allEvents = await fetchRepoAnnouncementsWithCache ( nostrClient , context . repoOwnerPubkey , eventCache ) ;
let announcement = findRepoAnnouncement ( allEvents , context . repo ) ;
// If no events found in cache/default relays, try all relays (default + search)
// Try all relays if not found
if ( ! announcement ) {
const allRelays = [ . . . new Set ( [ . . . DEFAULT_NOSTR_RELAYS , . . . DEFAULT_NOSTR_SEARCH_RELAYS ] ) ] ;
// Only create new client if we have additional relays to try
if ( allRelays . length > DEFAULT_NOSTR_RELAYS . length ) {
const allRelaysClient = new NostrClient ( allRelays ) ;
allEvents = await fetchRepoAnnouncementsWithCache ( allRelaysClient , context . repoOwnerPubkey , eventCache ) ;
@ -80,21 +81,19 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -80,21 +81,19 @@ export const GET: RequestHandler = createRepoGetHandler(
}
}
const events = announcement ? [ announcement ] : [ ] ;
if ( events . length > 0 ) {
// Try API-based fetching first (no cloning)
if ( announcement ) {
// Try API-based fetching
const { tryApiFetch } = await import ( '$lib/utils/api-repo-helper.js' ) ;
const { extractCloneUrls } = await import ( '$lib/utils/nostr-utils.js' ) ;
const cloneUrls = extractCloneUrls ( events [ 0 ] ) ;
const cloneUrls = extractCloneUrls ( announcement ) ;
logger . debug ( { npub : context.npub , repo : context.repo , cloneUrlCount : cloneUrls.length , cloneUrls } , 'Attempting API fallback for branches' ) ;
logger . debug ( { npub : context.npub , repo : context.repo , cloneUrlCount : cloneUrls.length } , 'Attempting API fallback for branches' ) ;
const apiData = await tryApiFetch ( events [ 0 ] , context . npub , context . repo ) ;
const apiData = await tryApiFetch ( announcement , context . npub , context . repo ) ;
if ( apiData && apiData . branches && apiData . branches . length > 0 ) {
logger . debug ( { npub : context.npub , repo : context.repo , branchCount : apiData.branches.length } , 'Successfully fetched branches via API fallback' ) ;
// Sort branches: default branch first, then alphabetically
// Sort branches: default branch first
const sortedBranches = [ . . . apiData . branches ] ;
if ( apiData . defaultBranch ) {
sortedBranches . sort ( ( a : any , b : any ) = > {
@ -108,18 +107,8 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -108,18 +107,8 @@ export const GET: RequestHandler = createRepoGetHandler(
return json ( sortedBranches ) ;
}
// API fetch failed - repo is not cloned and API fetch didn't work
// Check if we have clone URLs to provide better error message
// API fetch failed
const hasCloneUrls = cloneUrls . length > 0 ;
logger . warn ( {
npub : context.npub ,
repo : context.repo ,
hasCloneUrls ,
cloneUrlCount : cloneUrls.length ,
cloneUrls : cloneUrls.slice ( 0 , 3 ) // Log first 3 URLs for debugging
} , 'API fallback failed for branches - repo not cloned and API fetch unsuccessful' ) ;
// Provide more detailed error message
const cloneUrlTypes = cloneUrls . map ( url = > {
if ( url . includes ( 'github.com' ) ) return 'GitHub' ;
if ( url . includes ( 'gitlab.com' ) || url . includes ( 'gitlab' ) ) return 'GitLab' ;
@ -130,29 +119,22 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -130,29 +119,22 @@ export const GET: RequestHandler = createRepoGetHandler(
throw handleNotFoundError (
hasCloneUrls
? ` Repository is not cloned locally and could not be fetched via API from external clone URLs ( ${ cloneUrlTypes . join ( ', ' ) } ). This may be due to API rate limits, network issues, or the repository being private. Privileged users can clone this repository using the "Clone to Server" button. `
: 'Repository is not cloned locally and has no external clone URLs for API fallback. Privileged users can clone this repository using the "Clone to Server" button. ' ,
? ` Repository is not cloned locally and could not be fetched via API from external clone URLs ( ${ cloneUrlTypes . join ( ', ' ) } ). `
: 'Repository is not cloned locally and has no external clone URLs for API fallback.' ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
} else {
// No events found - could be because:
// 1. Repository doesn't exist
// 2. Relays are unreachable
// 3. Repository is on different relays
throw handleNotFoundError (
'Repository announcement not found in Nostr. This could mean: (1) the repository does not exist, (2) the configured Nostr relays are unreachable, or (3) the repository is published on different relays. Try configuring additional relays via the NOSTR_RELAYS environment variable. ' ,
'Repository announcement not found in Nostr.' ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
}
} catch ( err ) {
// Check if repo was created by another concurrent request
if ( existsSync ( repoPath ) ) {
// Repo exists now, clear cache and continue with normal flow
repoCache . delete ( RepoCache . repoExistsKey ( context . npub , context . repo ) ) ;
} else {
// Log the error for debugging
logger . error ( { error : err , npub : context.npub , repo : context.repo } , '[Branches] Error fetching repository' ) ;
// If fetching fails, return 404 with more context
const errorMessage = err instanceof Error ? err . message : 'Repository not found' ;
throw handleNotFoundError (
errorMessage ,
@ -162,27 +144,18 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -162,27 +144,18 @@ export const GET: RequestHandler = createRepoGetHandler(
}
}
// Double-check repo exists (should be true if we got here)
if ( ! existsSync ( repoPath ) ) {
throw handleNotFoundError (
'Repository not found' ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
}
// Repo exists, get branches
try {
const branches = await fileManager . getBranches ( context . npub , context . repo ) ;
// If repo exists but has no branches ( empty repo) , try API fallback
// If empty repo, try API fallback
if ( branches . length === 0 ) {
logger . debug ( { npub : context.npub , repo : context.repo } , 'Repo exists but is empty, attempting API fallback' ) ;
try {
// Fetch repository announcement for API fallback
let allEvents = await fetchRepoAnnouncementsWithCache ( nostrClient , context . repoOwnerPubkey , eventCache ) ;
let announcement = findRepoAnnouncement ( allEvents , context . repo ) ;
// If no events found in cache/default relays, try all relays (default + search)
if ( ! announcement ) {
const allRelays = [ . . . new Set ( [ . . . DEFAULT_NOSTR_RELAYS , . . . DEFAULT_NOSTR_SEARCH_RELAYS ] ) ] ;
if ( allRelays . length > DEFAULT_NOSTR_RELAYS . length ) {
@ -194,16 +167,10 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -194,16 +167,10 @@ export const GET: RequestHandler = createRepoGetHandler(
if ( announcement ) {
const { tryApiFetch } = await import ( '$lib/utils/api-repo-helper.js' ) ;
const { extractCloneUrls } = await import ( '$lib/utils/nostr-utils.js' ) ;
const cloneUrls = extractCloneUrls ( announcement ) ;
logger . debug ( { npub : context.npub , repo : context.repo , cloneUrlCount : cloneUrls.length } , 'Attempting API fallback for empty repo' ) ;
const apiData = await tryApiFetch ( announcement , context . npub , context . repo ) ;
if ( apiData && apiData . branches && apiData . branches . length > 0 ) {
logger . info ( { npub : context.npub , repo : context.repo , branchCount : apiData.branches.length } , 'Successfully fetched branches via API fallback for empty repo' ) ;
// Sort branches: default branch first, then alphabetically
const sortedBranches = [ . . . apiData . branches ] ;
if ( apiData . defaultBranch ) {
sortedBranches . sort ( ( a : any , b : any ) = > {
@ -218,17 +185,11 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -218,17 +185,11 @@ export const GET: RequestHandler = createRepoGetHandler(
}
}
} catch ( apiErr ) {
logger . debug ( { error : apiErr , npub : context.npub , repo : context.repo } , 'API fallback failed for empty repo, returning empty branches ' ) ;
logger . debug ( { error : apiErr , npub : context.npub , repo : context.repo } , 'API fallback failed for empty repo' ) ;
}
}
// If branches is still empty after API fallback, return empty array (empty repo is valid)
if ( branches . length === 0 ) {
logger . debug ( { npub : context.npub , repo : context.repo } , 'Repository is empty (no branches), returning empty array' ) ;
return json ( [ ] ) ;
}
// Sort branches: default branch first, then alphabetically
// Sort branches: default branch first
let sortedBranches = [ . . . branches ] ;
try {
const defaultBranch = await fileManager . getDefaultBranch ( context . npub , context . repo ) ;
@ -241,7 +202,6 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -241,7 +202,6 @@ export const GET: RequestHandler = createRepoGetHandler(
return aName . localeCompare ( bName ) ;
} ) ;
} else {
// No default branch found, just sort alphabetically
sortedBranches . sort ( ( a : any , b : any ) = > {
const aName = typeof a === 'string' ? a : a.name ;
const bName = typeof b === 'string' ? b : b.name ;
@ -249,7 +209,6 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -249,7 +209,6 @@ export const GET: RequestHandler = createRepoGetHandler(
} ) ;
}
} catch {
// If we can't get default branch, just sort alphabetically
sortedBranches . sort ( ( a : any , b : any ) = > {
const aName = typeof a === 'string' ? a : a.name ;
const bName = typeof b === 'string' ? b : b.name ;
@ -259,16 +218,13 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -259,16 +218,13 @@ export const GET: RequestHandler = createRepoGetHandler(
return json ( sortedBranches ) ;
} catch ( err ) {
// Log the actual error for debugging
logger . error ( { error : err , npub : context.npub , repo : context.repo } , '[Branches] Error getting branches' ) ;
// Check if it's a "not found" error
if ( err instanceof Error && err . message . includes ( 'not found' ) ) {
throw handleNotFoundError (
err . message ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
}
// Otherwise, it's a server error
throw handleApiError (
err ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo } ,
@ -276,34 +232,84 @@ export const GET: RequestHandler = createRepoGetHandler(
@@ -276,34 +232,84 @@ export const GET: RequestHandler = createRepoGetHandler(
) ;
}
} ,
{ operation : 'getBranches' , requireRepoExists : false , requireRepoAccess : true } // Handle on-demand fetching, but check access for private repos
{ operation : 'getBranches' , requireRepoExists : false , requireRepoAccess : true }
) ;
/ * *
* POST : Create a new branch
* /
export const POST : RequestHandler = createRepoPostHandler (
async ( context : RepoRequestContext , event : RequestEvent ) = > {
logger . info ( {
npub : context.npub ,
repo : context.repo ,
userPubkey : context.userPubkeyHex ? context . userPubkeyHex . substring ( 0 , 16 ) + '...' : null
} , '[Branches POST] ========== START ==========' ) ;
const body = await event . request . json ( ) ;
const { branchName , fromBranch , announcement } = body ;
logger . info ( { body , npub : context.npub , repo : context.repo } , '[Branches POST] Request body parsed' ) ;
const { branchName , fromBranch } = body ;
logger . info ( { branchName , fromBranch , npub : context.npub , repo : context.repo } , '[Branches POST] Extracted parameters' ) ;
if ( ! branchName ) {
logger . error ( { npub : context.npub , repo : context.repo } , '[Branches POST] Missing branchName parameter' ) ;
throw handleValidationError ( 'Missing branchName parameter' , { operation : 'createBranch' , npub : context.npub , repo : context.repo } ) ;
}
const repoPath = join ( repoRoot , context . npub , ` ${ context . repo } .git ` ) ;
logger . info ( { repoPath , npub : context.npub , repo : context.repo } , '[Branches POST] Repository path resolved' ) ;
const repoExists = existsSync ( repoPath ) ;
logger . info ( { repoExists , repoPath , npub : context.npub , repo : context.repo } , '[Branches POST] Repository existence checked' ) ;
// Authorization checks
if ( repoExists && context . userPubkeyHex ) {
const isMaintainer = await maintainerService . isMaintainer (
context . userPubkeyHex ,
context . repoOwnerPubkey ,
context . repo
) ;
if ( ! isMaintainer ) {
throw handleAuthorizationError (
'Only repository maintainers can create branches.' ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo }
) ;
}
} else if ( repoExists && ! context . userPubkeyHex ) {
throw handleAuthError (
'Authentication required to create branches in existing repositories.' ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo }
) ;
} else if ( ! repoExists && context . userPubkeyHex ) {
// New repo - verify user is the owner
const { requireNpubHex } = await import ( '$lib/utils/npub-utils.js' ) ;
const ownerPubkey = requireNpubHex ( context . npub ) ;
if ( context . userPubkeyHex . toLowerCase ( ) !== ownerPubkey . toLowerCase ( ) ) {
throw handleAuthorizationError (
'Only the repository owner can create the first branch in a new repository.' ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo }
) ;
}
} else if ( ! repoExists && ! context . userPubkeyHex ) {
throw handleAuthError (
'Authentication required to create branches.' ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo }
) ;
}
// 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' ) ;
// Check if repoRoot exists and is writable (with helpful container error messages)
// Check/create repoRoot
if ( ! existsSync ( repoRoot ) ) {
try {
await mkdir ( repoRoot , { recursive : true } ) ;
logger . debug ( { repoRoot } , 'Created repoRoot directory' ) ;
} catch ( rootErr ) {
logger . error ( { error : rootErr , repoRoot } , 'Failed to create repoRoot directory' ) ;
// Check if parent directory is writable
const parentRoot = dirname ( repoRoot ) ;
if ( existsSync ( parentRoot ) ) {
try {
@ -323,7 +329,6 @@ export const POST: RequestHandler = createRepoPostHandler(
@@ -323,7 +329,6 @@ export const POST: RequestHandler = createRepoPostHandler(
) ;
}
} else {
// Directory exists, check if it's writable
try {
checkDirectoryWritable ( repoRoot , 'GIT_REPO_ROOT directory' ) ;
} catch ( checkErr ) {
@ -341,7 +346,7 @@ export const POST: RequestHandler = createRepoPostHandler(
@@ -341,7 +346,7 @@ export const POST: RequestHandler = createRepoPostHandler(
await mkdir ( repoDir , { recursive : true } ) ;
logger . debug ( { repoDir } , 'Created repository directory' ) ;
} catch ( dirErr ) {
logger . error ( { error : dirErr , repoDir , npub : context.npub , repo : context.repo } , 'Failed to create repository directory' ) ;
logger . error ( { error : dirErr , repoDir } , 'Failed to create repository directory' ) ;
throw handleApiError (
dirErr ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo } ,
@ -351,12 +356,13 @@ export const POST: RequestHandler = createRepoPostHandler(
@@ -351,12 +356,13 @@ export const POST: RequestHandler = createRepoPostHandler(
// Initialize bare repository
try {
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' ) ;
// Clear cache
repoCache . delete ( RepoCache . repoExistsKey ( context . npub , context . repo ) ) ;
} catch ( initErr ) {
logger . error ( { error : initErr , repoPath , npub : context.npub , repo : context.repo } , 'Failed to initialize bare repository' ) ;
logger . error ( { error : initErr , repoPath } , 'Failed to initialize bare repository' ) ;
throw handleApiError (
initErr ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo } ,
@ -365,37 +371,127 @@ export const POST: RequestHandler = createRepoPostHandler(
@@ -365,37 +371,127 @@ export const POST: RequestHandler = createRepoPostHandler(
}
}
// Check if repo has any branches first
let hasBranches = false ;
// Check if repo has commits - use multiple verification methods
logger . info ( { npub : context.npub , repo : context.repo , repoPath } , '[Branches POST] Starting commit check' ) ;
let hasCommits = false ;
let commitCount = 0 ;
try {
const existingBranches = await fileManager . getBranches ( context . npub , context . repo ) ;
hasBranches = existingBranches . length > 0 ;
const git = simpleGit ( repoPath ) ;
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Git instance created for commit check' ) ;
try {
// Method 1: rev-list count
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Method 1: Running rev-list --count --all' ) ;
const commitCountStr = await git . raw ( [ 'rev-list' , '--count' , '--all' ] ) ;
commitCount = parseInt ( commitCountStr . trim ( ) , 10 ) ;
hasCommits = ! isNaN ( commitCount ) && commitCount > 0 ;
logger . info ( { npub : context.npub , repo : context.repo , commitCountStr , commitCount , hasCommits } , '[Branches POST] Method 1 result: rev-list completed' ) ;
// Method 2: Double-check by verifying refs exist
if ( hasCommits ) {
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Method 2: Checking refs (hasCommits=true)' ) ;
try {
const refs = await git . raw ( [ 'for-each-ref' , '--count=1' , 'refs/heads/' ] ) ;
logger . info ( { npub : context.npub , repo : context.repo , refs , refsLength : refs?.trim ( ) . length } , '[Branches POST] Method 2 result: refs checked' ) ;
if ( ! refs || refs . trim ( ) . length === 0 ) {
hasCommits = false ;
logger . warn ( { npub : context.npub , repo : context.repo } , '[Branches POST] No refs found despite commit count, treating as empty' ) ;
}
} catch ( refError ) {
hasCommits = false ;
logger . warn ( { npub : context.npub , repo : context.repo , error : refError } , '[Branches POST] Method 2 failed: ref check error' ) ;
}
} else {
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Skipping Method 2 (hasCommits=false)' ) ;
}
logger . info ( { npub : context.npub , repo : context.repo , commitCount , hasCommits } , '[Branches POST] Final commit check result' ) ;
} catch ( revListErr ) {
hasCommits = false ;
logger . info ( {
npub : context.npub ,
repo : context.repo ,
error : revListErr ,
errorMessage : revListErr instanceof Error ? revListErr.message : String ( revListErr )
} , '[Branches POST] rev-list failed (empty repo expected)' ) ;
}
} catch ( err ) {
// If getBranches fails, assume no branches exist
logger . debug ( { error : err , npub : context.npub , repo : context.repo } , 'Failed to get branches, assuming empty repo' ) ;
hasBranches = false ;
logger . warn ( {
error : err ,
errorMessage : err instanceof Error ? err.message : String ( err ) ,
npub : context.npub ,
repo : context.repo
} , '[Branches POST] Failed to check commits, assuming empty' ) ;
hasCommits = false ;
}
// Get default branch if fromBranch not provided and repo has branches
// If repo has no branches, don't pass fromBranch (will use --orphan)
let sourceBranch = fromBranch ;
if ( ! sourceBranch && hasBranches ) {
try {
sourceBranch = await fileManager . getDefaultBranch ( context . npub , context . repo ) ;
} catch ( err ) {
// If getDefaultBranch fails, use 'main' as default (only if branches exist)
logger . debug ( { error : err , npub : context.npub , repo : context.repo } , 'No default branch found, using main' ) ;
sourceBranch = 'main' ;
// Determine source branch - CRITICAL: If no commits, NEVER use a source branch
logger . info ( { npub : context.npub , repo : context.repo , hasCommits , fromBranch } , '[Branches POST] Starting source branch determination' ) ;
let sourceBranch : string | undefined = undefined ; // Start with undefined
if ( hasCommits ) {
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Repo has commits - checking for source branch' ) ;
// Only consider using a source branch if repo has commits
if ( fromBranch ) {
// User explicitly provided a source branch - use it (will be verified in createBranch)
sourceBranch = fromBranch ;
logger . info ( { npub : context.npub , repo : context.repo , sourceBranch } , '[Branches POST] Using provided fromBranch' ) ;
} else {
// Try to get default branch
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] No fromBranch provided - getting default branch' ) ;
try {
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Getting existing branches' ) ;
const existingBranches = await fileManager . getBranches ( context . npub , context . repo ) ;
logger . info ( { npub : context.npub , repo : context.repo , branchCount : existingBranches.length , branches : existingBranches } , '[Branches POST] Existing branches retrieved' ) ;
if ( existingBranches . length > 0 ) {
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Getting default branch' ) ;
sourceBranch = await fileManager . getDefaultBranch ( context . npub , context . repo ) ;
logger . info ( { npub : context.npub , repo : context.repo , sourceBranch } , '[Branches POST] Got default branch' ) ;
} else {
sourceBranch = undefined ;
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] No branches found, using undefined' ) ;
}
} catch ( err ) {
logger . warn ( {
error : err ,
errorMessage : err instanceof Error ? err.message : String ( err ) ,
npub : context.npub ,
repo : context.repo
} , '[Branches POST] Failed to get default branch' ) ;
sourceBranch = undefined ;
}
}
} else {
// No commits - sourceBranch stays undefined
logger . info ( { npub : context.npub , repo : context.repo , hasCommits } , '[Branches POST] Empty repo - sourceBranch will be undefined' ) ;
}
// If repo has no branches, sourceBranch will be undefined/null, which createBranch will handle correctly
// Final safety check - should never happen but be extra safe
if ( sourceBranch && ! hasCommits ) {
logger . error ( { sourceBranch , hasCommits , npub : context.npub , repo : context.repo } , '[Branches POST] ERROR: sourceBranch set but no commits! Clearing it.' ) ;
sourceBranch = undefined ;
}
logger . info ( {
npub : context.npub ,
repo : context.repo ,
branchName ,
sourceBranch ,
fromBranch ,
hasCommits ,
commitCount
} , '[Branches POST] ========== FINAL VALUES BEFORE createBranch CALL ==========' ) ;
logger . info ( { npub : context.npub , repo : context.repo , branchName , sourceBranch } , '[Branches POST] Calling fileManager.createBranch' ) ;
await fileManager . createBranch ( context . npub , context . repo , branchName , sourceBranch ) ;
logger . info ( { npub : context.npub , repo : context.repo , branchName } , '[Branches POST] ========== SUCCESS ==========' ) ;
return json ( { success : true , message : 'Branch created successfully' } ) ;
} ,
{ operation : 'createBranch' , requireRepoExists : false } // Allow creating branches in empty repos
{ operation : 'createBranch' , requireRepoExists : false , requireMaintainer : false }
) ;
/ * *
* DELETE : Delete a branch
* /
export const DELETE : RequestHandler = createRepoPostHandler (
async ( context : RepoRequestContext , event : RequestEvent ) = > {
const body = await event . request . json ( ) ;