Repository navigation
Description
Summary
A project can be identified as the wrong remote when the server inherits repository settings from another checkout. Repository discovery reads the selected project, but the query for its preferred remote reads the inherited checkout instead. A direct run of the real resolver reproduced the wrong identity; the same project resolves correctly with a clean environment.
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
- Create a project repository with
origin=https://github.com/example/fork.git,upstream=https://github.com/example/parent.git, andremote.origin.gh-resolved=base(the setting used bygh repo set-default). - Create a separate repository with
remote.upstream.gh-resolved=base. - Call the real project identity resolver for the first project, first without
GIT_DIR, then from a fresh resolver withGIT_DIRpointing at the separate repository’s.gitdirectory. The script below performs these steps.
Both the exploratory run and the isolated script run reproduced the failure. For an application-level trigger, start the server with the same inherited environment and add the first repository as a project; this entry point is traced in source rather than exercised in a running client.
Save the following as repro.sh and run sh repro.sh /path/to/t3code. Prerequisites: a clean checkout of the stated revision with its dependencies installed, Node.js 26, and Git on PATH. It creates repositories and all state under a temporary directory, makes no network connections, and removes its fixtures on exit. Exit 1 means the defect was reproduced, 0 means the expected behavior holds, and 2 means the control did not establish a valid setup.
Reproduction script: repro.sh
#!/bin/sh
set -eu
scratch=$(mktemp -d)
finished=0
cleanup() {
status=$?
rm -rf "$scratch"
if [ "$finished" -eq 0 ]; then
echo 'RESULT: inconclusive'
exit 2
fi
exit "$status"
}
trap cleanup EXIT
trap 'exit 2' HUP INT TERM
checkout=$(cd "$1" && pwd -P)
mkdir -p "$scratch/home" "$scratch/tmp" "$scratch/state"
export HOME="$scratch/home" TMPDIR="$scratch/tmp" T3CODE_HOME="$scratch/state"
export GIT_CONFIG_NOSYSTEM=1
unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_CEILING_DIRECTORIES
node --version
git --version
git -C "$checkout" rev-parse HEAD
cat > "$scratch/repro.mjs" <<'JS'
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
try {
const [checkout, scratch] = process.argv.slice(2);
const require = createRequire(join(checkout, 'apps/server/package.json'));
const load = (name) => import(pathToFileURL(require.resolve(name)).href);
const Effect = await load('effect/Effect');
const NodeServices = await load('@effect/platform-node/NodeServices');
const Resolver = await import(pathToFileURL(join(checkout, 'apps/server/src/project/RepositoryIdentityResolver.ts')).href);
const git = (cwd, ...args) => execFileSync('git', ['-C', cwd, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
const project = join(scratch, 'project'), other = join(scratch, 'other');
for (const cwd of [project, other]) { mkdirSync(cwd); git(cwd, 'init'); }
git(project, 'remote', 'add', 'origin', 'https://github.com/example/fork.git');
git(project, 'remote', 'add', 'upstream', 'https://github.com/example/parent.git');
git(project, 'config', 'remote.origin.gh-resolved', 'base');
git(other, 'config', 'remote.upstream.gh-resolved', 'base');
const read = () => Effect.runPromise(Effect.gen(function* () {
const resolver = yield* Resolver.RepositoryIdentityResolver;
return yield* resolver.resolve(project);
}).pipe(Effect.provide(Resolver.layer), Effect.provide(NodeServices.layer)));
const control = (await read())?.canonicalKey;
console.log('control:', control);
if (control !== 'github.com/example/fork') { console.log('RESULT: inconclusive'); process.exit(2); }
process.env.GIT_DIR = join(other, '.git');
const inherited = (await read())?.canonicalKey;
console.log('inherited GIT_DIR:', inherited);
if (inherited === control) { console.log('RESULT: expected behavior'); process.exit(0); }
if (inherited === 'github.com/example/parent') { console.log('RESULT: defect reproduced'); process.exit(1); }
console.log('RESULT: inconclusive'); process.exit(2);
} catch { console.log('RESULT: inconclusive'); process.exit(2); }
JS
set +e
node "$scratch/repro.mjs" "$checkout" "$scratch"
status=$?
finished=1
exit "$status"Expected behavior
The identity remains github.com/example/fork, with locator remote origin, in both runs. The project’s explicit gh default is the contract exercised by apps/server/src/project/RepositoryIdentityResolver.test.ts#L321-L341. The resolver’s own environment helper apps/server/src/project/RepositoryIdentityResolver.ts#L150-L153 unsets inherited repository selectors so discovery is based on the selected project. Git documents that GIT_DIR overrides repository discovery in git.adoc.
Expected source: the Git contract and existing resolver tests linked above.
Actual behavior
The real RepositoryIdentityResolver.resolve(project) returned github.com/example/parent with the server process’s GIT_DIR pointing to a second checkout, although the project sets remote.origin.gh-resolved=base and its origin is https://github.com/example/fork.git. A fresh resolver with the same project and no inherited selector returned github.com/example/fork. No network requests or real GitHub repositories are needed.
Impact
Minor bug or occasional failure
Impact: P2 - The server returns the wrong canonical repository for an affected project, while a safe workaround is to restart it without inherited repository selectors.
The reproduced case needs an inherited GIT_DIR and a project whose gh default differs from the fallback remote. The other checkout can supply a conflicting mark, or simply omit the project’s mark. Project grouping and pull-request matching consume this identity; their UI effects were traced in source, not exercised in a client. The clean-environment control verifies the workaround for the resolver.
Version or commit
main @ 0bb6eaa546c91915efbd3f092088c694f3949709. The failure was reproduced against this source checkout with its installed lockfile dependencies; the packaged desktop release was not exercised.
Environment
macOS (darwin), arm64; Node.js v26.12.1; Git 2.56.0. The reproduction imports the real server service and uses real local Git processes without contacting any remote.
Logs or stack traces
Evidence
Observed output from the isolated script (exit 1):
v26.12.1
git version 2.56.0
0bb6eaa546c91915efbd3f092088c694f3949709
control: github.com/example/fork
inherited GIT_DIR: github.com/example/parent
RESULT: defect reproduced
Failure source: observed module execution of the actual service, plus apps/server/src/project/RepositoryIdentityResolver.ts#L245-L278. remote -v uses gitEnv, while the separate config --get-regexp query uses only explicitBareRepositoryEnv and inherits GIT_DIR. The observed mismatch is therefore between the project’s remote list and another checkout’s default-remote mark.
Supported entry point: project enrichment calls the resolver at apps/server/src/project/ProjectEnrichmentService.ts#L114-L124; pull-request discovery calls it at apps/server/src/orchestration-v2/ThreadPullRequestService.ts#L79-L93.
Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete for the resolver failure. Missing fact: NOT_ENOUGH_INFO the downstream client presentation was not exercised; a client run against the same fixture would verify it. All paths are generated at runtime; the output contains no private machine paths.
Workaround
Start the server without inherited GIT_DIR (the clean-environment resolver control succeeds).
Restoration check
rerun the script against the candidate change. Both clean and inherited-environment lookups must return github.com/example/fork, and the script must print RESULT: expected behavior and exit 0.
References
- [Bug]: Started with GIT_DIR set, T3 Code shows another repository's branch in my project #35099 — the earlier inherited-selector failure was closed after general discovery and remote-list sanitization; the separate gh-default config query remains affected in the tested revision.
- fix(server): projects read their own Git repository when the server inherited GIT_DIR #35119 — sanitizes discovery and
remote -v, but the gh-default query still uses a different environment. - [Bug]: Project identity ignores gh repo set-default, so fork PRs don't match fork-based projects #15023 — establishes that project identity must follow the selected gh default; this report concerns that selection when the server inherits
GIT_DIR.
Activity