Browse Source

bug-fixes

Nostr-Signature: 32794e047f06902ad610f918834efb113f41eace26a53a3f0fad083b9d8323dc 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 3859f0de3de0f8a742b6fbe7709c5a5625f4d5612a936fd81f38a7e1231ee810b50a69c1ed5d23c8a6670b4cbc9ea3d4bd39d6fa9e6207802f45995689b924a9
main
Silberengel 2 weeks ago
parent
commit
a949fe6641
  1. 2
      nostr/commit-signatures.jsonl
  2. 49
      src/lib/services/git/announcement-manager.ts
  3. 174
      src/lib/services/git/repo-manager.ts
  4. 31
      src/routes/api/repos/[npub]/[repo]/clone/+server.ts
  5. 20
      src/routes/repos/[npub]/[repo]/services/repo-operations.ts

2
nostr/commit-signatures.jsonl

@ -118,3 +118,5 @@ @@ -118,3 +118,5 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772264490,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","polling update"]],"content":"Signed commit: polling update","id":"42c1a2a63a4568c65d82d78701451b3b4363bdf9c8c57e804535b5f3f0d7b6fc","sig":"8e5f32ecb79da876ac41eba04c3b1541b21d039ae50d1b9fefa630d35f31c97dd29af64e4b695742fa7d4eaec17db8f4a066b4db99ce628aed596971975d4a87"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772267611,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","refactor API"]],"content":"Signed commit: refactor API","id":"934f8809638cea0bc7b8158fca959bc60880e0cae9ab8ff653687313adcd2f57","sig":"c9d8e5b821ae8182f8d39599c50fd0a4db6040ead1d8d83730a608a1d94d5078770a6ccbfc525a98691e98fabd9f9d24f0298680fb564c6b76c2f34bed9889b5"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772269280,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","api refactor part 2"]],"content":"Signed commit: api refactor part 2","id":"ece894a60057bba46ebd4ac0dca2aca55ffce05e44671fe07b29516809fc86f6","sig":"176706a271659834e441ea5eab4bb1480667dad4468fe8315803284f4a183debf595523dd33d0d3cabe0c35013f4a72b9169b5f10afefaf8a82a721d8b0f3b08"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772270859,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes and fallback relay"]],"content":"Signed commit: bug-fixes and fallback relay","id":"1d85d0c5e1451c90bca5d59e08043f29adeaad4db4ac5495c8e9a4247775780f","sig":"a1960b76c78db9f64dad20378d26f500ffc09f1f6d137314db548470202712222a1d391f682146ba281fd23355c574fcbb260310db61b3458bba3dec0c724a18"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772271656,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"f4a5e0d3e2aa7d0d99803f26008ab68e40551e36362bb6d04acf639c5b78d959","sig":"59da9e59a6fb5648f4c889e0045b571e0d2d66a555100d60dec373455309a640bea89e4bb3a42a0e502aa4d2091e4b698203721e79b346ff30e6b2bcdc5f48b3"}

49
src/lib/services/git/announcement-manager.ts

@ -190,7 +190,7 @@ export class AnnouncementManager { @@ -190,7 +190,7 @@ export class AnnouncementManager {
* Ensure announcement event is saved to nostr/repo-events.jsonl in the repository
* Only saves if not already present (avoids redundant entries)
*/
async ensureAnnouncementInRepo(repoPath: string, event: NostrEvent, selfTransferEvent?: NostrEvent): Promise<void> {
async ensureAnnouncementInRepo(repoPath: string, event: NostrEvent, selfTransferEvent?: NostrEvent, preferredDefaultBranch?: string): Promise<void> {
let isEmpty = false;
try {
// Create a temporary working directory
@ -218,8 +218,8 @@ export class AnnouncementManager { @@ -218,8 +218,8 @@ export class AnnouncementManager {
if (isEmpty) {
// Repo is empty - initialize worktree and create initial branch
// Use default branch from environment or try 'main' first, then 'master'
const defaultBranch = process.env.DEFAULT_BRANCH || 'main';
// Use preferred branch, then environment, then try 'main' first, then 'master'
const defaultBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'main';
// Initialize git in workdir
workGit = simpleGit(workDir);
@ -233,6 +233,45 @@ export class AnnouncementManager { @@ -233,6 +233,45 @@ export class AnnouncementManager {
await git.clone(repoPath, workDir);
// Create workGit instance after clone
workGit = simpleGit(workDir);
// Determine the correct default branch to commit to
let targetBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'main';
// Check if the preferred branch exists, if not try to find the actual default branch
try {
const branches = await workGit.branch(['-a']);
const branchList = branches.all
.map(b => b.replace(/^remotes\/origin\//, '').replace(/^remotes\//, '').replace(/^refs\/heads\//, ''))
.filter(b => b && !b.includes('HEAD'));
// If preferred branch exists, use it; otherwise try main/master, then first branch
if (preferredDefaultBranch && branchList.includes(preferredDefaultBranch)) {
targetBranch = preferredDefaultBranch;
} else if (branchList.includes('main')) {
targetBranch = 'main';
} else if (branchList.includes('master')) {
targetBranch = 'master';
} else if (branchList.length > 0) {
targetBranch = branchList[0];
}
// Checkout the target branch
try {
await workGit.checkout(targetBranch);
logger.debug({ repoPath, targetBranch }, 'Checked out target branch for announcement commit');
} catch (checkoutErr) {
// If checkout fails, try to create the branch
logger.debug({ repoPath, targetBranch, error: checkoutErr }, 'Failed to checkout branch, will try to create it');
try {
await workGit.checkout(['-b', targetBranch]);
logger.debug({ repoPath, targetBranch }, 'Created and checked out new branch for announcement commit');
} catch (createErr) {
logger.warn({ repoPath, targetBranch, error: createErr }, 'Failed to create branch, will commit to current branch');
}
}
} catch (branchErr) {
logger.warn({ repoPath, error: branchErr }, 'Failed to determine or checkout branch, will commit to current branch');
}
}
// Check if announcement already exists in nostr/repo-events.jsonl
@ -316,8 +355,8 @@ export class AnnouncementManager { @@ -316,8 +355,8 @@ export class AnnouncementManager {
logger.info({ repoPath, commitHash, objectCount: objectEntries.length }, 'Objects verified after commit');
// Push back to bare repo
// Use default branch from environment or try 'main' first, then 'master'
const defaultBranch = process.env.DEFAULT_BRANCH || 'main';
// Use preferred branch, then environment, then try 'main' first, then 'master'
const defaultBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'main';
if (isEmpty) {
// For empty repos, directly copy objects and update refs (more reliable than push)

174
src/lib/services/git/repo-manager.ts

@ -71,8 +71,9 @@ export class RepoManager { @@ -71,8 +71,9 @@ export class RepoManager {
* @param selfTransferEvent - Optional self-transfer event to include in initial commit
* @param isExistingRepo - Whether this is an existing repo being added to the server
* @param allowMissingDomainUrl - In development, allow provisioning even if domain URL isn't in announcement
* @param preferredDefaultBranch - Preferred default branch name (e.g., from user settings)
*/
async provisionRepo(event: NostrEvent, selfTransferEvent?: NostrEvent, isExistingRepo: boolean = false, allowMissingDomainUrl: boolean = false): Promise<void> {
async provisionRepo(event: NostrEvent, selfTransferEvent?: NostrEvent, isExistingRepo: boolean = false, allowMissingDomainUrl: boolean = false, preferredDefaultBranch?: string): Promise<void> {
const cloneUrls = this.urlParser.extractCloneUrls(event);
let domainUrl = cloneUrls.find(url => url.includes(this.domain));
@ -161,16 +162,22 @@ export class RepoManager { @@ -161,16 +162,22 @@ export class RepoManager {
if (!hasBranches) {
// No branches exist - create initial branch and README (which includes announcement)
await this.createInitialBranchAndReadme(repoPath.fullPath, repoPath.npub, repoPath.repoName, event);
await this.createInitialBranchAndReadme(repoPath.fullPath, repoPath.npub, repoPath.repoName, event, preferredDefaultBranch);
} else {
// Branches exist (from sync) - ensure announcement is committed to the default branch
// Branches exist (from sync) - ensure README exists and announcement is committed to the default branch
// Check if README exists, and create it if missing
await this.ensureReadmeExists(repoPath.fullPath, repoPath.npub, repoPath.repoName, event, preferredDefaultBranch);
// Ensure announcement is committed to the default branch
// This must happen after syncing so we can commit it to the existing default branch
// Non-blocking: fire and forget - we have the announcement from relays, so this is just for offline papertrail
this.announcementManager.ensureAnnouncementInRepo(repoPath.fullPath, event, selfTransferEvent)
.catch((err) => {
// Make it blocking so the commit is complete before returning
try {
await this.announcementManager.ensureAnnouncementInRepo(repoPath.fullPath, event, selfTransferEvent, preferredDefaultBranch);
logger.info({ repoPath: repoPath.fullPath, eventId: event.id }, 'Announcement committed to repository');
} catch (err) {
logger.warn({ error: err, repoPath: repoPath.fullPath, eventId: event.id },
'Failed to save announcement to repo (non-blocking, announcement available from relays)');
});
'Failed to save announcement to repo (announcement available from relays)');
}
}
} else {
// For existing repos, check if announcement exists in repo
@ -204,12 +211,13 @@ export class RepoManager { @@ -204,12 +211,13 @@ export class RepoManager {
repoPath: string,
npub: string,
repoName: string,
announcementEvent: NostrEvent
announcementEvent: NostrEvent,
preferredDefaultBranch?: string
): Promise<void> {
try {
// Get default branch from git config, environment, or use 'master'
// Check git's init.defaultBranch config first (respects user's git settings)
let defaultBranch = process.env.DEFAULT_BRANCH || 'master';
// Get default branch from preferred branch, git config, environment, or use 'master'
// Check preferred branch first (from user settings), then git's init.defaultBranch config
let defaultBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'master';
try {
const git = simpleGit();
@ -233,8 +241,14 @@ export class RepoManager { @@ -233,8 +241,14 @@ export class RepoManager {
// If branches exist, check if one matches our default branch preference
if (existingBranches.length > 0) {
// Prefer existing branches that match common defaults
const preferredBranches = [defaultBranch, 'main', 'master', 'dev'];
// If we have a preferred branch and it exists, use it
if (preferredDefaultBranch && existingBranches.includes(preferredDefaultBranch)) {
defaultBranch = preferredDefaultBranch;
} else {
// Prefer existing branches that match common defaults, prioritizing preferred branch
const preferredBranches = preferredDefaultBranch
? [preferredDefaultBranch, defaultBranch, 'main', 'master', 'dev']
: [defaultBranch, 'main', 'master', 'dev'];
for (const preferred of preferredBranches) {
if (existingBranches.includes(preferred)) {
defaultBranch = preferred;
@ -246,6 +260,7 @@ export class RepoManager { @@ -246,6 +260,7 @@ export class RepoManager {
defaultBranch = existingBranches[0];
}
}
}
} catch {
// No branches exist, use the determined default
}
@ -337,6 +352,130 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -337,6 +352,130 @@ Your commits will all be signed by your Nostr keys and saved to the event files
}
}
/**
* Ensure README.md exists in the repository, creating it if missing
* This is called when branches exist from sync but README might be missing
*/
private async ensureReadmeExists(
repoPath: string,
npub: string,
repoName: string,
announcementEvent: NostrEvent,
preferredDefaultBranch?: string
): Promise<void> {
try {
// Get default branch
const { FileManager } = await import('./file-manager.js');
const fileManager = new FileManager(this.repoRoot);
let defaultBranch = preferredDefaultBranch;
if (!defaultBranch) {
try {
defaultBranch = await fileManager.getDefaultBranch(npub, repoName);
} catch {
// If getDefaultBranch fails, try to determine from git
const repoGit = simpleGit(repoPath);
try {
const branches = await repoGit.branch(['-a']);
const branchList = branches.all
.map(b => b.replace(/^remotes\/origin\//, '').replace(/^remotes\//, '').replace(/^refs\/heads\//, ''))
.filter(b => b && !b.includes('HEAD'));
if (branchList.length > 0) {
// Prefer preferred branch, then main, then master, then first branch
if (preferredDefaultBranch && branchList.includes(preferredDefaultBranch)) {
defaultBranch = preferredDefaultBranch;
} else if (branchList.includes('main')) {
defaultBranch = 'main';
} else if (branchList.includes('master')) {
defaultBranch = 'master';
} else {
defaultBranch = branchList[0];
}
}
} catch {
defaultBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'master';
}
}
}
if (!defaultBranch) {
defaultBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'master';
}
// Check if README.md already exists
const workDir = await fileManager.getWorktree(repoPath, defaultBranch, npub, repoName);
const { readFile: readFileFs, writeFile: writeFileFs } = await import('fs/promises');
const { join } = await import('path');
const readmePath = join(workDir, 'README.md');
try {
await readFileFs(readmePath, 'utf-8');
// README exists, nothing to do
await fileManager.removeWorktree(repoPath, workDir);
logger.debug({ npub, repoName, branch: defaultBranch }, 'README.md already exists');
return;
} catch {
// README doesn't exist, create it
}
// Get repo name from d-tag or use repoName from path
const dTag = announcementEvent.tags.find(t => t[0] === 'd')?.[1] || repoName;
// Get name tag for README title, fallback to d-tag
const nameTag = announcementEvent.tags.find(t => t[0] === 'name')?.[1] || dTag;
// Get author info from user profile (fetch from relays)
const { fetchUserProfile, extractProfileData, getUserName, getUserEmail } = await import('../../utils/user-profile.js');
const { nip19 } = await import('nostr-tools');
const { DEFAULT_NOSTR_RELAYS } = await import('../../config.js');
const userNpub = nip19.npubEncode(announcementEvent.pubkey);
const profileEvent = await fetchUserProfile(announcementEvent.pubkey, DEFAULT_NOSTR_RELAYS);
const profile = extractProfileData(profileEvent);
const authorName = getUserName(profile, announcementEvent.pubkey, userNpub);
const authorEmail = getUserEmail(profile, announcementEvent.pubkey, userNpub);
// Create README.md content
const readmeContent = `# ${nameTag}
Welcome to your new GitRepublic repo.
You can use this read-me file to explain the purpose of this repo to everyone who looks at it. You can also make a ReadMe.adoc file and delete this one, if you prefer. GitRepublic supports both markups.
Your commits will all be signed by your Nostr keys and saved to the event files in the ./nostr folder.
`;
// Write README.md
await writeFileFs(readmePath, readmeContent, 'utf-8');
// Stage and commit README.md
const workGit = simpleGit(workDir);
await workGit.add('README.md');
// Configure git user.name and user.email for this repository
try {
await workGit.addConfig('user.name', 'GitRepublic', false, 'local');
await workGit.addConfig('user.email', 'gitrepublic@gitrepublic.web', false, 'local');
} catch (configError) {
logger.warn({ repoPath, npub, repoName, error: configError }, 'Failed to set git config');
}
// Commit README.md
await workGit.commit('Add README.md', ['README.md'], {
'--author': `${authorName} <${authorEmail}>`
});
// Clean up worktree
await fileManager.removeWorktree(repoPath, workDir);
logger.info({ npub, repoName, branch: defaultBranch }, 'Created README.md in existing repository');
} catch (err) {
// Log but don't fail - README creation is nice-to-have
const sanitizedErr = sanitizeError(err);
logger.warn({ error: sanitizedErr, repoPath, npub, repoName }, 'Failed to ensure README exists, continuing anyway');
}
}
/**
* Sync repository from multiple remote URLs (parallelized for efficiency)
*/
@ -370,7 +509,8 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -370,7 +509,8 @@ Your commits will all be signed by your Nostr keys and saved to the event files
async fetchRepoOnDemand(
npub: string,
repoName: string,
announcementEvent?: NostrEvent
announcementEvent?: NostrEvent,
preferredDefaultBranch?: string
): Promise<{ success: boolean; needsAnnouncement?: boolean; announcement?: NostrEvent; error?: string; cloneUrls?: string[]; remoteUrls?: string[] }> {
const repoPath = join(this.repoRoot, npub, `${repoName}.git`);
@ -518,7 +658,7 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -518,7 +658,7 @@ Your commits will all be signed by your Nostr keys and saved to the event files
// No remote URLs - this is an empty repo, provision it instead
logger.info({ npub, repoName, cloneUrls, announcementEventId: announcementEvent.id }, 'No remote clone URLs found - provisioning empty repository');
try {
await this.provisionRepo(announcementEvent, undefined, false);
await this.provisionRepo(announcementEvent, undefined, false, false, preferredDefaultBranch);
logger.info({ npub, repoName }, 'Empty repository provisioned successfully');
return { success: true, cloneUrls, remoteUrls: [] };
} catch (err) {
@ -539,7 +679,7 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -539,7 +679,7 @@ Your commits will all be signed by your Nostr keys and saved to the event files
logger.info({ npub, repoName, url: remoteUrls[0] }, 'Localhost URL specified but repo does not exist locally - provisioning instead');
try {
// In development, allow provisioning even if domain URL isn't in announcement
await this.provisionRepo(announcementEvent, undefined, false, true);
await this.provisionRepo(announcementEvent, undefined, false, true, preferredDefaultBranch);
logger.info({ npub, repoName }, 'Repository provisioned successfully on localhost');
return { success: true, cloneUrls, remoteUrls: [] };
} catch (err) {

31
src/routes/api/repos/[npub]/[repo]/clone/+server.ts

@ -51,6 +51,30 @@ export const POST: RequestHandler = async (event) => { @@ -51,6 +51,30 @@ export const POST: RequestHandler = async (event) => {
hasUnlimitedAccess: userLevel ? hasUnlimitedAccess(userLevel.level) : false
}, 'Checking user access level for clone operation');
// Extract defaultBranch from request body if present (before body is consumed)
let preferredDefaultBranch: string | undefined;
const contentType = event.request.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
try {
// Clone the request to read body without consuming it
const clonedRequest = event.request.clone();
const bodyText = await clonedRequest.text().catch(() => '');
if (bodyText) {
try {
const body = JSON.parse(bodyText);
if (body.defaultBranch && typeof body.defaultBranch === 'string') {
preferredDefaultBranch = body.defaultBranch;
logger.debug({ preferredDefaultBranch }, 'Extracted defaultBranch from request body');
}
} catch {
// Not valid JSON or missing defaultBranch - continue
}
}
} catch {
// Body reading failed - continue
}
}
// If cache is empty, try to verify from NIP-98 auth header first (doesn't consume body), then proof event in body
if (!userLevel || !hasUnlimitedAccess(userLevel.level)) {
let verification: { valid: boolean; error?: string; relay?: string; relayDown?: boolean } | null = null;
@ -66,7 +90,6 @@ export const POST: RequestHandler = async (event) => { @@ -66,7 +90,6 @@ export const POST: RequestHandler = async (event) => {
// If auth header didn't work, try to get proof event from request body (if content-type is JSON)
// Note: This consumes the body, but only if auth header is not present
if (!verification) {
const contentType = event.request.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
try {
// Read body only if auth header verification failed
@ -85,6 +108,10 @@ export const POST: RequestHandler = async (event) => { @@ -85,6 +108,10 @@ export const POST: RequestHandler = async (event) => {
logger.warn({ userPubkeyHex: userPubkeyHex.slice(0, 16) + '...' }, 'Invalid proof event in request body');
}
}
// Also extract defaultBranch if not already extracted
if (!preferredDefaultBranch && body.defaultBranch && typeof body.defaultBranch === 'string') {
preferredDefaultBranch = body.defaultBranch;
}
} catch (parseErr) {
// Not valid JSON or missing proofEvent - continue
logger.debug({ error: parseErr }, 'Request body is not valid JSON or missing proofEvent');
@ -276,7 +303,7 @@ export const POST: RequestHandler = async (event) => { @@ -276,7 +303,7 @@ export const POST: RequestHandler = async (event) => {
}, 'Repository announcement clone URLs');
// Attempt to clone the repository
const result = await repoManager.fetchRepoOnDemand(npub, repo, announcementEvent);
const result = await repoManager.fetchRepoOnDemand(npub, repo, announcementEvent, preferredDefaultBranch);
if (!result.success) {
if (result.needsAnnouncement) {

20
src/routes/repos/[npub]/[repo]/services/repo-operations.ts

@ -193,11 +193,27 @@ export async function cloneRepository( @@ -193,11 +193,27 @@ export async function cloneRepository(
logger.debug({ error: proofErr }, '[Clone] Failed to create proof event, will rely on cache');
}
// Send clone request with proof event in body (if available)
const requestBody: { proofEvent?: NostrEvent } = {};
// Get user's default branch preference from settings
let defaultBranch: string | undefined;
try {
const { settingsStore } = await import('$lib/services/settings-store.js');
const settings = await settingsStore.getSettings();
if (settings.defaultBranch) {
defaultBranch = settings.defaultBranch;
logger.debug({ defaultBranch }, '[Clone] Using default branch from user settings');
}
} catch (settingsErr) {
logger.debug({ error: settingsErr }, '[Clone] Failed to get default branch from settings');
}
// Send clone request with proof event and defaultBranch in body (if available)
const requestBody: { proofEvent?: NostrEvent; defaultBranch?: string } = {};
if (proofEvent) {
requestBody.proofEvent = proofEvent;
}
if (defaultBranch) {
requestBody.defaultBranch = defaultBranch;
}
const data = await apiPost<{ alreadyExists?: boolean }>(`/api/repos/${state.npub}/${state.repo}/clone`, requestBody);

Loading…
Cancel
Save