From f04e798084dfc6e67cd74fa2e3e51d04235e8e41 Mon Sep 17 00:00:00 2001 From: Bryandero98 Date: Thu, 3 Sep 2026 18:10:27 -0500 Subject: [PATCH] fix: classify a fork by owner alone, so renaming it doesn't disable fork detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit classifyOriginRemote() required both a different owner AND a matching repo name to classify an origin as a fork. Renaming a fork on GitHub (/PortOS -> /PortOS-Fork) silently flipped isFork to false — git/gh repo sync keep working through GitHub's slug redirect, but the fork panel, POST /api/update/sync-fork, the FORK_SYNC_REQUIRED update guard, and the divergence check all disappear with no warning. Drops the repo-name requirement: any GitHub origin under an owner other than upstream now classifies as a fork. The tradeoff (a genuinely unrelated GitHub origin also classifies as a fork) is accepted deliberately — its failure mode is a clear gh repo sync error, not four silently missing features. Refs #5931 --- server/lib/gitRemote.js | 11 +++++++---- server/lib/gitRemote.test.js | 20 ++++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/server/lib/gitRemote.js b/server/lib/gitRemote.js index cf4cd07b79..6466561fa8 100644 --- a/server/lib/gitRemote.js +++ b/server/lib/gitRemote.js @@ -151,10 +151,13 @@ export function classifyOriginRemote(originUrl, { const ownerMatchesUpstream = parsed.owner.toLowerCase() === upstreamOwner.toLowerCase(); const repoMatchesUpstream = parsed.repo.toLowerCase() === upstreamRepo.toLowerCase(); const isUpstream = isGithub && ownerMatchesUpstream && repoMatchesUpstream; - // Strict fork: same repo name, different owner, on GitHub. A renamed - // GitHub repo (different name) doesn't count — `gh repo sync` would fail - // and the fork-aware UI would mislead the user. - const isFork = isGithub && repoMatchesUpstream && !ownerMatchesUpstream; + // Fork: different owner than upstream, on GitHub — regardless of repo name. + // A renamed fork (`/PortOS` -> `/PortOS-Fork`) still IS the + // PortOS checkout; requiring the name to match `repoMatchesUpstream` made + // `isFork` silently go false on rename, taking the fork panel, the + // sync-fork route, and the divergence check with it while `git`/`gh repo + // sync` kept working fine through GitHub's slug redirect (#5931). + const isFork = isGithub && !ownerMatchesUpstream; return { hasOrigin: true, diff --git a/server/lib/gitRemote.test.js b/server/lib/gitRemote.test.js index d01eb772ac..efac2f5c3a 100644 --- a/server/lib/gitRemote.test.js +++ b/server/lib/gitRemote.test.js @@ -193,17 +193,29 @@ describe('getOriginInfo', () => { }); }); - it('does NOT classify as fork when the repo name differs (renamed/unrelated)', async () => { - // Someone might fork-and-rename, or point origin at an unrelated GitHub - // repo. Treating that as a fork would invoke `gh repo sync` and fail. + it('classifies as fork by owner alone, regardless of repo name (#5931)', async () => { + // A fork renamed on GitHub (`/PortOS` -> `/PortOS-Fork`) still IS the + // PortOS checkout — GitHub's slug redirect keeps `git`/`gh repo sync` working, and a + // strict repo-name match silently dropped the fork panel, the sync-fork route, and + // the divergence check the moment someone renamed. The tradeoff: an origin genuinely + // unrelated to PortOS also classifies as a fork now — an acceptable false positive + // whose failure mode is a clear `gh repo sync` error, not four silently missing + // features. execGit.mockResolvedValue({ stdout: 'git@github.com:alice/MyCustomOS.git\n', stderr: '', exitCode: 0 }); const info = await getOriginInfo(); expect(info.isGithub).toBe(true); - expect(info.isFork).toBe(false); + expect(info.isFork).toBe(true); expect(info.isUpstream).toBe(false); expect(info.fullName).toBe('alice/MyCustomOS'); }); + it('classifies a renamed fork of PortOS itself as a fork', async () => { + execGit.mockResolvedValue({ stdout: 'git@github.com:alice/PortOS-Fork.git\n', stderr: '', exitCode: 0 }); + const info = await getOriginInfo(); + expect(info.isFork).toBe(true); + expect(info.isUpstream).toBe(false); + }); + it('classifies upstream even when origin URL carries a port', async () => { execGit.mockResolvedValue({ stdout: 'ssh://git@github.com:443/atomantic/PortOS.git\n', stderr: '', exitCode: 0 }); const info = await getOriginInfo();