Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- From a fresh clone of pingdotgg/t3code, check out
ddc4f576023b3bf7ead6c831510c1d72316bb1c2. Runexport T3CODE_HOME="$(mktemp -d)"andexport VERIFIER_T3CODE_HOME="$T3CODE_HOME", thenvp i --frozen-lockfile -- --yes < /dev/null. - Save the following independent test as
apps/server/src/project/RepositoryPathQuery.verifier.test.ts. It initializes real temporary Git repositories, adds each origin, verifies Git returns that exact URL, invokes the production resolver, and sends the result to the production selector and GitHub PR-match predicate.
import * as NodeServices from "@effect/platform-node/NodeServices";
import { expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import { repositoryPathFromRemoteUrl } from "@t3tools/source-control-core/server/SourceControlProvider";
import * as ProcessRunner from "../processRunner.ts";
import { definition as github } from "@t3tools/source-control-github/shared/definition";
import { sourceControlDefinitions } from "../sourceControl/sourceControlDefinitions.ts";
import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts";
process.env.T3CODE_HOME = process.env.VERIFIER_T3CODE_HOME;
const remotes = [
["plain control", "https://github.com/owner/repo.git"],
["query", "https://github.com/owner/repo.git?view=compact"],
["fragment", "https://github.com/owner/repo.git#section"],
] as const;
it.each(remotes)("parser: %s", (_label, remote) => {
expect(repositoryPathFromRemoteUrl(remote)).toBe("owner/repo");
});
it.layer(NodeServices.layer)("real git repository identity", (it) => {
for (const [_label, remote] of remotes) {
it.effect(`resolver: ${_label}`, () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "repository-path-query-" });
const runner = yield* ProcessRunner.ProcessRunner;
for (const args of [["init"], ["remote", "add", "origin", remote]]) {
const result = yield* runner.run({ command: "git", args: ["-C", cwd, ...args] });
expect(result.code).toBe(0);
}
const remoteResult = yield* runner.run({ command: "git", args: ["-C", cwd, "remote", "get-url", "origin"] });
expect(remoteResult.stdout.trim()).toBe(remote);
const resolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;
const identity = yield* resolver.resolve(cwd);
const observed = {
canonicalKey: identity?.canonicalKey,
displayName: identity?.displayName,
name: identity?.name,
selector: sourceControlDefinitions.repositorySelector(identity),
matchesPullRequest: identity !== null && github.isChangeRequestInRepository(identity,
{ host: "github.com", repository: "owner/repo", number: 1 }),
};
console.log(JSON.stringify({ label: _label, ...observed }));
expect(observed).toEqual({ canonicalKey: "github.com/owner/repo",
displayName: "owner/repo", name: "repo", selector: "owner/repo", matchesPullRequest: true });
}).pipe(Effect.provide(RepositoryIdentityResolver.layer), Effect.provide(ProcessRunner.layer)),
);
}
});- Run
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 vp test run apps/server/src/project/RepositoryPathQuery.verifier.test.ts. Disabling global/system Git configuration prevents local insteadOf rules from changing the HTTPS input. The test restores the isolated T3CODE_HOME after the existing test setup clears that variable. - Observe 2 passing plain-URL controls and 4 failing assertions: query and fragment variants fail both the direct parser and the real-Git identity/selector/PR-match checks. Remove the temporary test file after reproducing.
The final isolated run reproduced at the baseline and again at current upstream 62dc589d6dff48c73002842e4603c5ab89a6880f, with the same 2-pass/4-fail outcome. Each test run covers both URL variants and their passing control. Reproduction does not require that the example repository exist remotely; the failure occurs while identifying an already configured checkout, before any remote operation.
Expected behavior
A URL's query and fragment should not turn a clone transport suffix into part of the repository identity. All three remote spellings in this reproduction should identify github.com/owner/repo, select owner/repo, and match that repository's PR references. The repository-path tests and remote-normalization tests explicitly omit .git from the path and canonical key. The resolver identity test ties the canonical key, display name, owner, and name to the repository rather than its transport syntax. The URL-shaped remote parser uses URL.pathname as the repository path; the tested inputs reach the real resolver without a query/fragment rejection.
Actual behavior
I observed RepositoryIdentityResolver.resolve return canonicalKey github.com/owner/repo.git, displayName owner/repo.git, and name repo.git for a real local Git repository with origin https://github.com/owner/repo.git?view=compact. The repository selector becomes owner/repo.git, and GitHub's repository-membership predicate returns false for a PR reference to owner/repo. Origin https://github.com/owner/repo.git#section produces the same incorrect identity. A plain https://github.com/owner/repo.git remote passes the identical identity, selector, and PR-match assertions.
The underlying direct repositoryPathFromRemoteUrl call also returns owner/repo.git?view=compact or owner/repo.git#section. The resolver's canonical-path fallback removes the query or fragment from its displayed identity, but still leaves .git. These observations use benign URL components and disposable repositories; no credentials, GitHub network requests, server, or UI were involved.
Impact
Minor bug or occasional failure
Version or commit
ddc4f57; also reproduced after a frozen install at upstream 62dc589.
Environment
Node.js v26.12.1, Darwin arm64, repository-pinned vite-plus package 1.4.0 with test runner v5.0.5.
Logs or stack traces
The focused test command above produced these reduced output records and summary in both the baseline and upstream runs:
{"label":"query","canonicalKey":"github.com/owner/repo.git","displayName":"owner/repo.git","name":"repo.git","selector":"owner/repo.git","matchesPullRequest":false}
{"label":"fragment","canonicalKey":"github.com/owner/repo.git","displayName":"owner/repo.git","name":"repo.git","selector":"owner/repo.git","matchesPullRequest":false}
Tests 4 failed | 2 passed (6)
Source: lowerCaseRemote and URL parsing remove a terminal .git before parsing the URL, so the suffix survives when followed by a query or fragment. The direct path extractor also consumes the trailing URL components. The resolver fallback and identity construction choose the canonical path after the direct result disagrees, leaving .git in the resolved name. The default selector returns that display name, and GitHub's predicate delegates to the path equality check, which rejects owner/repo against owner/repo.git.
Workaround
No response
Impact and scope
Impact: P2 - HTTPS origins with .git immediately followed by a query or fragment receive an incorrect canonical identity and repository selector, and fail the repository-membership predicate for their actual PRs.
The defect is bounded to affected remote spellings; the plain-URL controls pass. Observed downstream behavior is the wrong selector and false membership result, not an authenticated GitHub API failure. Source inspection also shows thread PR discovery comparing canonical repository keys and deriving this selector. Using the plain URL avoids the reproduced error when the extra components are unnecessary; whether changing a particular real remote preserves its authentication or routing is NOT_ENOUGH_INFO and requires that remote's configuration and transport requirements. No live PR workflow, browser, or other operating system was exercised.
Interface
Library: server RepositoryIdentityResolver, shared source-control repository selector, and GitHub repository-membership predicate.
Earliest failure stage
Input parsing or validation: remote URL normalization and repository-path extraction retain the clone suffix when URL components follow it.
Additional context
The baseline and upstream production refinement hooks do not restore the GitHub identity: GitHub has no refineRepositoryIdentity hook, and the Cursor Origin, GitLab, and Forgejo hooks leave a GitHub identity unchanged. Only the Forgejo login-discovery code changed in the expanded relevant upstream path check; the same focused reproduction still fails upstream. This report does not claim that GitHub supports query-string token authentication. It concerns the identity of a URL accepted from an existing Git remote.
Related issues
none found matching this behavior in open and closed issues and PRs. Related parser work includes #31917 (SCP IPv6 hosts) and #29457 (userless SCP remotes); neither covers this URL suffix case.
Activity