Skip to content

Commit 8ebf1ed

Browse files
Fix peer-remote detection + pin DreamNode branch to main
Two related cross-machine bugs surfaced by the reverse-direction (Windows-initiates) demo: 1. GitSyncService only recognized peer remotes whose URL was rad:// or interbrain://. But the clone-accept sovereignty handover renames the cloned origin to a peer remote keeping git's NATIVE https:// URL — so "Check for Updates" reported "No peer remotes found" and never fetched the peer's beacon. Fixed: a peer remote is simply any remote that isn't `origin`. The interbrain:// scheme is for .gitmodules (shared, portable content needing UUID indirection), not local remote config which is already concrete. 2. DreamNode creation did `git init` with no -b flag, inheriting the user's init.defaultBranch. Windows machines often still default to `master`; Mac to `main`. A master DreamNode and a main one can't see each other's commits. Fixed: all four `git init` call sites now use `git -c init.defaultBranch=main init`. #76 (auto-link), #81 (branch pin) — #81 resolved here; #80, #78, #79 still pending for the next rc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a5250ee commit 8ebf1ed

5 files changed

Lines changed: 31 additions & 13 deletions

File tree

src/features/dreamnode/services/dreamnode-conversion-service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ export class DreamNodeConversionService {
267267
'dreamnode',
268268
'DreamNode-template'
269269
);
270-
await execAsync(`git init --template="${templatePath}" "${folderPath}"`);
270+
// Pin default branch to `main` (the user's git config may default to
271+
// `master`, which breaks peer fetch — see git-dreamnode-service.ts).
272+
await execAsync(`git -c init.defaultBranch=main init --template="${templatePath}" "${folderPath}"`);
271273

272274
// Make hooks executable (no-op on Windows; Git for Windows runs
273275
// `#!/bin/sh` hooks regardless of the +x bit).

src/features/dreamnode/services/git-dreamnode-service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,12 @@ export class GitDreamNodeService {
480480
// Create directory
481481
await fsPromises.mkdir(repoPath, { recursive: true });
482482

483-
// Initialize git with template
484-
await execAsync(`git init --template="${this.templatePath}" "${repoPath}"`);
483+
// Initialize git with template. Pin the default branch to `main`
484+
// via `-c init.defaultBranch=main` — the user's git config may
485+
// still default to `master` (pre-2.28 git, or never changed),
486+
// which would break peer fetch: a `master` DreamNode and a `main`
487+
// one can't see each other's commits.
488+
await execAsync(`git -c init.defaultBranch=main init --template="${this.templatePath}" "${repoPath}"`);
485489

486490
// Make sure git can attribute the initial commit. Fresh Windows installs
487491
// usually have no global user.{name,email}; without these, `git commit`

src/features/dreamnode/utils/git-utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,13 @@ export async function commitAllChanges(repoPath: string, commitMessage: string):
234234
*/
235235
export async function initRepo(repoPath: string, templatePath?: string): Promise<void> {
236236
try {
237+
// `-c init.defaultBranch=main` pins the branch regardless of the
238+
// user's git config (which may still default to `master`). A master
239+
// DreamNode and a main one can't see each other's commits on fetch.
237240
if (templatePath) {
238-
await execAsync(`git init --template="${templatePath}"`, { cwd: repoPath });
241+
await execAsync(`git -c init.defaultBranch=main init --template="${templatePath}"`, { cwd: repoPath });
239242
} else {
240-
await execAsync('git init', { cwd: repoPath });
243+
await execAsync('git -c init.defaultBranch=main init', { cwd: repoPath });
241244
}
242245
} catch (error) {
243246
throw new Error(`Failed to init repo: ${error instanceof Error ? error.message : 'Unknown error'}`);

src/features/dreamnode/utils/repo-initializer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ export async function createRepoDirectory(repoPath: string): Promise<void> {
4545
* Initialize a git repository with a template
4646
*/
4747
export async function initGitWithTemplate(repoPath: string, templatePath: string): Promise<void> {
48-
await execAsync(`git init --template="${templatePath}" "${repoPath}"`);
48+
// Pin default branch to `main` — see git-dreamnode-service.ts for why
49+
// (peer fetch breaks when a master DreamNode meets a main one).
50+
await execAsync(`git -c init.defaultBranch=main init --template="${templatePath}" "${repoPath}"`);
4951

5052
// Make hooks executable (no-op on Windows; Git for Windows runs
5153
// `#!/bin/sh` hooks regardless of the +x bit).

src/features/social-resonance-filter/services/git-sync-service.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,20 +189,27 @@ export class GitSyncService {
189189
throw fetchError;
190190
}
191191

192-
// ALSO fetch from peer remotes (for pure p2p collaboration)
192+
// ALSO fetch from peer remotes (for pure p2p collaboration).
193+
//
194+
// A peer remote is simply "any remote that isn't `origin`". `origin`
195+
// is the user's own sovereign outbox; every other remote is a peer
196+
// whose version we may want to pull from. We do NOT sniff for a URL
197+
// scheme: peer remotes are plain `https://github.com/...` URLs (the
198+
// clone-accept sovereignty handover renames the cloned origin to a
199+
// peer remote, keeping git's native URL). The `interbrain://` scheme
200+
// is reserved for `.gitmodules` — shared, portable content that
201+
// needs UUID indirection — not for local remote config, which is
202+
// already concrete.
193203
console.log(`GitSyncService: Checking for peer remotes...`);
194204
const { stdout: remoteVerbose } = await execAsync('git remote -v', { cwd: fullPath });
195205
const peerRemotes = new Set<string>();
196206

197-
// Match either legacy rad:// URLs or v0.16+ interbrain:// URLs.
198-
// Both are peer remotes; both go through the appropriate transport.
199-
const peerRemoteRegex = /^(\S+)\s+(rad:\/\/\S+|interbrain:\/\/\S+)/;
207+
// `git remote -v` lines: "<name>\t<url> (fetch|push)"
200208
for (const line of remoteVerbose.split('\n')) {
201-
const match = line.match(peerRemoteRegex);
209+
const match = line.match(/^(\S+)\s+\S+\s+\((?:fetch|push)\)$/);
202210
if (match) {
203211
const peerName = match[1];
204-
// Skip `origin` if it happens to match (it shouldn't, but be safe).
205-
if (peerName === 'origin') continue;
212+
if (peerName === 'origin') continue; // origin = own outbox, not a peer
206213
peerRemotes.add(peerName);
207214
}
208215
}

0 commit comments

Comments
 (0)