@ -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' ;
import { json } from '@sveltejs/kit' ;
// @ts-ignore - SvelteKit generates this type
import type { RequestHandler } from './$types' ;
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 { createRepoGetHandler , createRepoPostHandler } from '$lib/utils/api-handlers.js' ;
import type { RepoRequestContext , RequestEvent } from '$lib/utils/api-context.js' ;
import type { RepoRequestContext , RequestEvent } from '$lib/utils/api-context.js' ;
import { handleValidationError , handleApiError , handleNotFoundError } from '$lib/utils/error-handler.js' ;
import { handleValidationError , handleApiError , handleNotFoundError , handleAuthError , handleAuthorizationError } from '$lib/utils/error-handler.js' ;
import { KIND } from '$lib/types/nostr.js' ;
import { join , dirname , resolve } from 'path' ;
import { join , dirname , resolve } from 'path' ;
import { existsSync , accessSync , constants } from 'fs' ;
import { existsSync , accessSync , constants } from 'fs' ;
import { repoCache , RepoCache } from '$lib/services/git/repo-cache.js' ;
import { repoCache , RepoCache } from '$lib/services/git/repo-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 { fetchRepoAnnouncementsWithCache , findRepoAnnouncement } from '$lib/utils/nostr-utils.js' ;
import { isGraspUrl } from '$lib/services/git/api-repo-fetcher.js' ;
import { isGraspUrl } from '$lib/services/git/api-repo-fetcher.js' ;
import logger from '$lib/services/logger.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
* Check if a directory exists and is writable
* Provides helpful error messages for container environments
* /
* /
function checkDirectoryWritable ( dirPath : string , description : string ) : void {
function checkDirectoryWritable ( dirPath : string , description : string ) : void {
if ( ! existsSync ( dirPath ) ) {
if ( ! existsSync ( dirPath ) ) {
const isContainer = existsSync ( '/.dockerenv' ) || process . env . DOCKER_CONTAINER === 'true' ;
const isContainer = existsSync ( '/.dockerenv' ) || process . env . DOCKER_CONTAINER === 'true' ;
const errorMsg = isContainer
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 } ` ;
: ` ${ description } does not exist at ${ dirPath } ` ;
throw new Error ( errorMsg ) ;
throw new Error ( errorMsg ) ;
}
}
@ -38,41 +43,37 @@ function checkDirectoryWritable(dirPath: string, description: string): void {
} catch ( accessErr ) {
} catch ( accessErr ) {
const isContainer = existsSync ( '/.dockerenv' ) || process . env . DOCKER_CONTAINER === 'true' ;
const isContainer = existsSync ( '/.dockerenv' ) || process . env . DOCKER_CONTAINER === 'true' ;
const errorMsg = isContainer
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 ` ;
: ` ${ description } at ${ dirPath } is not writable ` ;
throw new Error ( errorMsg ) ;
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
* GET : List branches in a repository
? process . env . GIT_REPO_ROOT
* /
: '/repos' ;
const repoRoot = resolve ( repoRootEnv ) ;
export const GET : RequestHandler = createRepoGetHandler (
export const GET : RequestHandler = createRepoGetHandler (
async ( context : RepoRequestContext , event : RequestEvent ) = > {
async ( context : RepoRequestContext , event : RequestEvent ) = > {
const repoPath = join ( repoRoot , context . npub , ` ${ context . repo } .git ` ) ;
const repoPath = join ( repoRoot , context . npub , ` ${ context . repo } .git ` ) ;
const skipApiFallback = event . url . searchParams . get ( 'skipApiFallback' ) === 'true' ;
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 ( ! existsSync ( repoPath ) ) {
// If skipApiFallback is true, return 404 immediately to indicate repo is not cloned
if ( skipApiFallback ) {
if ( skipApiFallback ) {
throw handleNotFoundError (
throw handleNotFoundError (
'Repository is not cloned locally' ,
'Repository is not cloned locally' ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
) ;
}
}
try {
try {
// Fetch repository announcement (case-insensitive) with caching
// Fetch repository announcement
let allEvents = await fetchRepoAnnouncementsWithCache ( nostrClient , context . repoOwnerPubkey , eventCache ) ;
let allEvents = await fetchRepoAnnouncementsWithCache ( nostrClient , context . repoOwnerPubkey , eventCache ) ;
let announcement = findRepoAnnouncement ( allEvents , context . repo ) ;
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 ) {
if ( ! announcement ) {
const allRelays = [ . . . new Set ( [ . . . DEFAULT_NOSTR_RELAYS , . . . DEFAULT_NOSTR_SEARCH_RELAYS ] ) ] ;
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 ) {
if ( allRelays . length > DEFAULT_NOSTR_RELAYS . length ) {
const allRelaysClient = new NostrClient ( allRelays ) ;
const allRelaysClient = new NostrClient ( allRelays ) ;
allEvents = await fetchRepoAnnouncementsWithCache ( allRelaysClient , context . repoOwnerPubkey , eventCache ) ;
allEvents = await fetchRepoAnnouncementsWithCache ( allRelaysClient , context . repoOwnerPubkey , eventCache ) ;
@ -80,21 +81,19 @@ export const GET: RequestHandler = createRepoGetHandler(
}
}
}
}
const events = announcement ? [ announcement ] : [ ] ;
if ( announcement ) {
// Try API-based fetching
if ( events . length > 0 ) {
// Try API-based fetching first (no cloning)
const { tryApiFetch } = await import ( '$lib/utils/api-repo-helper.js' ) ;
const { tryApiFetch } = await import ( '$lib/utils/api-repo-helper.js' ) ;
const { extractCloneUrls } = await import ( '$lib/utils/nostr-utils.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 ) {
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' ) ;
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 ] ;
const sortedBranches = [ . . . apiData . branches ] ;
if ( apiData . defaultBranch ) {
if ( apiData . defaultBranch ) {
sortedBranches . sort ( ( a : any , b : any ) = > {
sortedBranches . sort ( ( a : any , b : any ) = > {
@ -108,18 +107,8 @@ export const GET: RequestHandler = createRepoGetHandler(
return json ( sortedBranches ) ;
return json ( sortedBranches ) ;
}
}
// API fetch failed - repo is not cloned and API fetch didn't work
// API fetch failed
// Check if we have clone URLs to provide better error message
const hasCloneUrls = cloneUrls . length > 0 ;
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 = > {
const cloneUrlTypes = cloneUrls . map ( url = > {
if ( url . includes ( 'github.com' ) ) return 'GitHub' ;
if ( url . includes ( 'github.com' ) ) return 'GitHub' ;
if ( url . includes ( 'gitlab.com' ) || url . includes ( 'gitlab' ) ) return 'GitLab' ;
if ( url . includes ( 'gitlab.com' ) || url . includes ( 'gitlab' ) ) return 'GitLab' ;
@ -130,29 +119,22 @@ export const GET: RequestHandler = createRepoGetHandler(
throw handleNotFoundError (
throw handleNotFoundError (
hasCloneUrls
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 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. 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.' ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
) ;
} else {
} else {
// No events found - could be because:
// 1. Repository doesn't exist
// 2. Relays are unreachable
// 3. Repository is on different relays
throw handleNotFoundError (
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 }
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
) ;
}
}
} catch ( err ) {
} catch ( err ) {
// Check if repo was created by another concurrent request
// Check if repo was created by another concurrent request
if ( existsSync ( repoPath ) ) {
if ( existsSync ( repoPath ) ) {
// Repo exists now, clear cache and continue with normal flow
repoCache . delete ( RepoCache . repoExistsKey ( context . npub , context . repo ) ) ;
repoCache . delete ( RepoCache . repoExistsKey ( context . npub , context . repo ) ) ;
} else {
} else {
// Log the error for debugging
logger . error ( { error : err , npub : context.npub , repo : context.repo } , '[Branches] Error fetching repository' ) ;
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' ;
const errorMessage = err instanceof Error ? err . message : 'Repository not found' ;
throw handleNotFoundError (
throw handleNotFoundError (
errorMessage ,
errorMessage ,
@ -162,27 +144,18 @@ export const GET: RequestHandler = createRepoGetHandler(
}
}
}
}
// Double-check repo exists (should be true if we got here)
// Repo exists, get branches
if ( ! existsSync ( repoPath ) ) {
throw handleNotFoundError (
'Repository not found' ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
}
try {
try {
const branches = await fileManager . getBranches ( context . npub , context . repo ) ;
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 ) {
if ( branches . length === 0 ) {
logger . debug ( { npub : context.npub , repo : context.repo } , 'Repo exists but is empty, attempting API fallback' ) ;
logger . debug ( { npub : context.npub , repo : context.repo } , 'Repo exists but is empty, attempting API fallback' ) ;
try {
try {
// Fetch repository announcement for API fallback
let allEvents = await fetchRepoAnnouncementsWithCache ( nostrClient , context . repoOwnerPubkey , eventCache ) ;
let allEvents = await fetchRepoAnnouncementsWithCache ( nostrClient , context . repoOwnerPubkey , eventCache ) ;
let announcement = findRepoAnnouncement ( allEvents , context . repo ) ;
let announcement = findRepoAnnouncement ( allEvents , context . repo ) ;
// If no events found in cache/default relays, try all relays (default + search)
if ( ! announcement ) {
if ( ! announcement ) {
const allRelays = [ . . . new Set ( [ . . . DEFAULT_NOSTR_RELAYS , . . . DEFAULT_NOSTR_SEARCH_RELAYS ] ) ] ;
const allRelays = [ . . . new Set ( [ . . . DEFAULT_NOSTR_RELAYS , . . . DEFAULT_NOSTR_SEARCH_RELAYS ] ) ] ;
if ( allRelays . length > DEFAULT_NOSTR_RELAYS . length ) {
if ( allRelays . length > DEFAULT_NOSTR_RELAYS . length ) {
@ -194,16 +167,10 @@ export const GET: RequestHandler = createRepoGetHandler(
if ( announcement ) {
if ( announcement ) {
const { tryApiFetch } = await import ( '$lib/utils/api-repo-helper.js' ) ;
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 ) ;
const apiData = await tryApiFetch ( announcement , context . npub , context . repo ) ;
if ( apiData && apiData . branches && apiData . branches . length > 0 ) {
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' ) ;
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 ] ;
const sortedBranches = [ . . . apiData . branches ] ;
if ( apiData . defaultBranch ) {
if ( apiData . defaultBranch ) {
sortedBranches . sort ( ( a : any , b : any ) = > {
sortedBranches . sort ( ( a : any , b : any ) = > {
@ -218,17 +185,11 @@ export const GET: RequestHandler = createRepoGetHandler(
}
}
}
}
} catch ( apiErr ) {
} 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)
// Sort branches: default branch first
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
let sortedBranches = [ . . . branches ] ;
let sortedBranches = [ . . . branches ] ;
try {
try {
const defaultBranch = await fileManager . getDefaultBranch ( context . npub , context . repo ) ;
const defaultBranch = await fileManager . getDefaultBranch ( context . npub , context . repo ) ;
@ -241,7 +202,6 @@ export const GET: RequestHandler = createRepoGetHandler(
return aName . localeCompare ( bName ) ;
return aName . localeCompare ( bName ) ;
} ) ;
} ) ;
} else {
} else {
// No default branch found, just sort alphabetically
sortedBranches . sort ( ( a : any , b : any ) = > {
sortedBranches . sort ( ( a : any , b : any ) = > {
const aName = typeof a === 'string' ? a : a.name ;
const aName = typeof a === 'string' ? a : a.name ;
const bName = typeof b === 'string' ? b : b.name ;
const bName = typeof b === 'string' ? b : b.name ;
@ -249,7 +209,6 @@ export const GET: RequestHandler = createRepoGetHandler(
} ) ;
} ) ;
}
}
} catch {
} catch {
// If we can't get default branch, just sort alphabetically
sortedBranches . sort ( ( a : any , b : any ) = > {
sortedBranches . sort ( ( a : any , b : any ) = > {
const aName = typeof a === 'string' ? a : a.name ;
const aName = typeof a === 'string' ? a : a.name ;
const bName = typeof b === 'string' ? b : b.name ;
const bName = typeof b === 'string' ? b : b.name ;
@ -259,16 +218,13 @@ export const GET: RequestHandler = createRepoGetHandler(
return json ( sortedBranches ) ;
return json ( sortedBranches ) ;
} catch ( err ) {
} catch ( err ) {
// Log the actual error for debugging
logger . error ( { error : err , npub : context.npub , repo : context.repo } , '[Branches] Error getting branches' ) ;
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' ) ) {
if ( err instanceof Error && err . message . includes ( 'not found' ) ) {
throw handleNotFoundError (
throw handleNotFoundError (
err . message ,
err . message ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
{ operation : 'getBranches' , npub : context.npub , repo : context.repo }
) ;
) ;
}
}
// Otherwise, it's a server error
throw handleApiError (
throw handleApiError (
err ,
err ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo } ,
{ operation : 'getBranches' , npub : context.npub , repo : context.repo } ,
@ -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 (
export const POST : RequestHandler = createRepoPostHandler (
async ( context : RepoRequestContext , event : RequestEvent ) = > {
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 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 ) {
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 } ) ;
throw handleValidationError ( 'Missing branchName parameter' , { operation : 'createBranch' , npub : context.npub , repo : context.repo } ) ;
}
}
const repoPath = join ( repoRoot , context . npub , ` ${ context . repo } .git ` ) ;
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 ) ;
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
// Create repo if it doesn't exist
if ( ! repoExists ) {
if ( ! repoExists ) {
logger . info ( { npub : context.npub , repo : context.repo } , 'Creating new empty repository for branch creation' ) ;
logger . info ( { npub : context.npub , repo : context.repo } , 'Creating new empty repository for branch creation' ) ;
const { mkdir } = await import ( 'fs/promises' ) ;
const { mkdir } = await import ( 'fs/promises' ) ;
// Check if repoRoot exists and is writable (with helpful container error messages)
// Check/create repoRoot
if ( ! existsSync ( repoRoot ) ) {
if ( ! existsSync ( repoRoot ) ) {
try {
try {
await mkdir ( repoRoot , { recursive : true } ) ;
await mkdir ( repoRoot , { recursive : true } ) ;
logger . debug ( { repoRoot } , 'Created repoRoot directory' ) ;
logger . debug ( { repoRoot } , 'Created repoRoot directory' ) ;
} catch ( rootErr ) {
} catch ( rootErr ) {
logger . error ( { error : rootErr , repoRoot } , 'Failed to create repoRoot directory' ) ;
logger . error ( { error : rootErr , repoRoot } , 'Failed to create repoRoot directory' ) ;
// Check if parent directory is writable
const parentRoot = dirname ( repoRoot ) ;
const parentRoot = dirname ( repoRoot ) ;
if ( existsSync ( parentRoot ) ) {
if ( existsSync ( parentRoot ) ) {
try {
try {
@ -323,7 +329,6 @@ export const POST: RequestHandler = createRepoPostHandler(
) ;
) ;
}
}
} else {
} else {
// Directory exists, check if it's writable
try {
try {
checkDirectoryWritable ( repoRoot , 'GIT_REPO_ROOT directory' ) ;
checkDirectoryWritable ( repoRoot , 'GIT_REPO_ROOT directory' ) ;
} catch ( checkErr ) {
} catch ( checkErr ) {
@ -341,7 +346,7 @@ export const POST: RequestHandler = createRepoPostHandler(
await mkdir ( repoDir , { recursive : true } ) ;
await mkdir ( repoDir , { recursive : true } ) ;
logger . debug ( { repoDir } , 'Created repository directory' ) ;
logger . debug ( { repoDir } , 'Created repository directory' ) ;
} catch ( dirErr ) {
} 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 (
throw handleApiError (
dirErr ,
dirErr ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo } ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo } ,
@ -351,12 +356,13 @@ export const POST: RequestHandler = createRepoPostHandler(
// Initialize bare repository
// Initialize bare repository
try {
try {
const simpleGit = ( await import ( 'simple-git' ) ) . default ;
const git = simpleGit ( ) ;
const git = simpleGit ( ) ;
await git . init ( [ '--bare' , repoPath ] ) ;
await git . init ( [ '--bare' , repoPath ] ) ;
logger . info ( { npub : context.npub , repo : context.repo } , 'Empty repository created successfully' ) ;
logger . info ( { npub : context.npub , repo : context.repo } , 'Empty repository created successfully' ) ;
// Clear cache
repoCache . delete ( RepoCache . repoExistsKey ( context . npub , context . repo ) ) ;
} catch ( initErr ) {
} 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 (
throw handleApiError (
initErr ,
initErr ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo } ,
{ operation : 'createBranch' , npub : context.npub , repo : context.repo } ,
@ -365,37 +371,127 @@ export const POST: RequestHandler = createRepoPostHandler(
}
}
}
}
// Check if repo has any branches first
// Check if repo has commits - use multiple verification methods
let hasBranches = false ;
logger . info ( { npub : context.npub , repo : context.repo , repoPath } , '[Branches POST] Starting commit check' ) ;
let hasCommits = false ;
let commitCount = 0 ;
try {
try {
const existingBranches = await fileManager . getBranches ( context . npub , context . repo ) ;
const git = simpleGit ( repoPath ) ;
hasBranches = existingBranches . length > 0 ;
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 ) {
} catch ( err ) {
// If getBranches fails, assume no branches exist
logger . warn ( {
logger . debug ( { error : err , npub : context.npub , repo : context.repo } , 'Failed to get branches, assuming empty repo' ) ;
error : err ,
hasBranches = false ;
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
// Determine source branch - CRITICAL: If no commits, NEVER use a source branch
// If repo has no branches, don't pass fromBranch (will use --orphan)
logger . info ( { npub : context.npub , repo : context.repo , hasCommits , fromBranch } , '[Branches POST] Starting source branch determination' ) ;
let sourceBranch = fromBranch ;
let sourceBranch : string | undefined = undefined ; // Start with undefined
if ( ! sourceBranch && hasBranches ) {
try {
if ( hasCommits ) {
sourceBranch = await fileManager . getDefaultBranch ( context . npub , context . repo ) ;
logger . info ( { npub : context.npub , repo : context.repo } , '[Branches POST] Repo has commits - checking for source branch' ) ;
} catch ( err ) {
// Only consider using a source branch if repo has commits
// If getDefaultBranch fails, use 'main' as default (only if branches exist)
if ( fromBranch ) {
logger . debug ( { error : err , npub : context.npub , repo : context.repo } , 'No default branch found, using main' ) ;
// User explicitly provided a source branch - use it (will be verified in createBranch)
sourceBranch = 'main' ;
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' ) ;
}
// 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 ;
}
}
// If repo has no branches, sourceBranch will be undefined/null, which createBranch will handle correctly
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 ) ;
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' } ) ;
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 (
export const DELETE : RequestHandler = createRepoPostHandler (
async ( context : RepoRequestContext , event : RequestEvent ) = > {
async ( context : RepoRequestContext , event : RequestEvent ) = > {
const body = await event . request . json ( ) ;
const body = await event . request . json ( ) ;