Skip to content

[Bug]: Inherited GIT_DIR changes a project’s selected repository despite its own gh default #35744

Closed

Description

@coygeek

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

  1. Create a project repository with origin=https://github.com/example/fork.git, upstream=https://github.com/example/parent.git, and remote.origin.gh-resolved=base (the setting used by gh repo set-default).
  2. Create a separate repository with remote.upstream.gh-resolved=base.
  3. Call the real project identity resolver for the first project, first without GIT_DIR, then from a fresh resolver with GIT_DIR pointing at the separate repository’s .git directory. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions