Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions server/lib/gitRemote.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<owner>/PortOS` -> `<owner>/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,
Expand Down
20 changes: 16 additions & 4 deletions server/lib/gitRemote.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<owner>/PortOS` -> `<owner>/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();
Expand Down