Skip to content

setup-project.py: a store credential helper holding no login for github.com gets the preflight's [??] and its gh auth setup-git offer, as a machine with no helper does, and the check no longer says every configured helper answers git's question for github.com (#992) - #1008

Merged
brettheap merged 1 commit into
mainfrom
l3-992-credential-helper-check
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Closes #992.

What changed

setup-project.py said that store and every other configured helper "answer git's question for github.com", and it counted any configured helper as a login. A helper that holds nothing answers nothing: store over an empty ~/.git-credentials answers nothing, and with git's prompt off (every scaffold push runs that way since #971, #915) git cannot read a username, so the first push fails after the three repositories exist (RV971's NIT-3 on #971).

The words first.

  • The old title "Will ANYTHING answer git's credential question for github.com?" (_credential_helper_configured) and the docstring line "they all answer git's question" are gone. A grep of both files for either phrase finds nothing.
  • The new docstrings say that a configured helper is not a stored login, and that store, the one kind measured, answers only with a login stored in it. They say nothing of what other helpers answer.
  • In tests/test_setup_project_py.py, test_a_credential_helper_that_is_not_ghs_is_left_alone changed only in its docstring; its name and body are as they were.

Then the check says it too, in the narrow form the issue offered.

  • Where every helper in the two keys it already read (credential.https://githubproxy.fjygbaifeng.eu.org.helper, then credential.helper) is exactly store, the check reads the files git's store reads:
    • store alone: ~/.git-credentials, then $XDG_CONFIG_HOME/git/credentials (or ~/.config/git/credentials where that is unset or empty);
    • store --file=<path> or store --file <path>: that one file, where the path is absolute and sh reads it as itself (SHELL_BARE_POSIX).
  • Where none of those files holds a login for https://github.com, it prints [??] git's credential helper for github.com is `store`, and no login for github.com is stored in <files>, so the first push over HTTPS will fail. Fix it once: gh auth setup-git and makes the same offer as for no helper. After gh auth setup-git it asks again.
  • Lines are read the way git reads them (credential_from_url): scheme exactly https, a user and a password before the first @, the host github.com after %XX decoding, no port, any path. A probe of git 2.43's own git credential-store get over 22 made-up lines agreed with that reading, and a test row asks git's own credential-store for 16 lines.
  • Left alone, as today: any other helper, or store beside one (including gh's helper over store); cache; a --file path the shell would change, a relative one, or another spelling such as --fil=; a file that exists but cannot be read.
  • git credential fill is never run. It runs every configured helper, and a helper may open a window or ask at a keychain. A file's lines are read for the one question and never printed.
  • Kept: the warning stays out of the checked list, so --preflight exits 0 either way (a warning that refuses is a refusal); a dry run still warns and offers nothing (setup-project.py: a passed-through -- --dry-run still offers gh auth setup-git, which writes the person's global git config on a typed yes, and checked_ref passes a.lock/b, which git refuses #857); the no-helper sentence is byte-identical.
  • No code, flag or --help line changed, so AGENTS.md, docs/cli.md and the handbook are untouched. setup-project.py is not a pinned copy, so no project re-pins.

The four decisions

  1. Same two keys as before. A helper configured under another spelling of github.com's URL stays invisible to both warnings.
  2. HOME unset: the check falls back to os.path.expanduser. Git for Windows may use HOMEDRIVE/HOMEPATH instead; that is not measured.
  3. Blanks at either end of a line count as a login (the check stays silent). Git keeps the blanks and then finds no login there; counting one costs silence, never a warning git would contradict.
  4. An unreadable file is left alone, although git's store dies on a folder there (measured, rc=128). The check can say nothing about a file it cannot read.

Out of scope

Tests

Every run was under a stub gh that exits 97 first on PATH, GH_TOKEN=bogus, an empty GH_CONFIG_DIR, HOME and XDG_CONFIG_HOME in scratch, /usr/bin/python3 -m pytest -q -p no:cacheprovider --basetemp=<scratch>/bt-<name> -o tmp_path_retention_policy=failed, one process at a time, at head b2dbc8b2 (the 14 files from the F992 run also under timeout 900, GIT_SSH_COMMAND=false, EMAIL set). No file hung and none is environment-blocked.

New rows, all in tests/test_setup_project_py.py: test_a_key_holding_only_an_empty_line_is_no_credential_helper; test_a_credential_store_with_no_github_login_is_warned_about (6 cases); test_a_dry_run_warns_of_an_empty_credential_store_and_offers_nothing; test_a_credential_store_holding_a_github_login_is_left_alone (6); test_a_credential_helper_whose_logins_this_cannot_read_is_left_alone (6); test_a_credential_store_file_is_read_as_git_reads_it (16 lines, asks git's own credential-store); test_an_empty_credential_store_is_asked_about_again_after_setup_git (2).

File Result
tests/test_setup_project_py.py 286 passed in 104.56s
tests/test_setup_project_dry_run_offers.py 13 passed in 1.64s
tests/test_setup_sh.py 129 passed in 73.23s
tests/test_setup_project_typed_name.py 22 passed in 3.71s
tests/test_setup_project_name_edges.py 97 passed in 16.71s
tests/test_setup_project_ref_parts.py 27 passed in 0.56s
tests/test_setup_family_folder.py 16 passed in 11.78s
tests/test_setup_by_hand_commands.py 5 passed in 0.13s
tests/test_branch_left_bump_leg_setup.py 19 passed in 25.02s
tests/test_github_limits_init_check_org.py 93 passed in 22.32s
tests/test_github_name_limits.py 64 passed in 18.20s
tests/test_doctor_machine_row.py 8 passed in 2.86s
tests/test_family_printed_quoting.py 84 passed in 11.01s
tests/test_git_bash_dialect.py 93 passed in 0.31s
tests/test_openreposhape_package.py 177 passed in 24.03s
tests/test_family_switch_branch_exists.py 25 passed in 25.31s
tests/test_validator_child_encoding.py 163 passed in 32.08s
tests/test_gh_guard.py 8 passed in 4.06s
tests/test_doctor_next_is_a_command.py 61 passed in 74.81s
tests/test_openreposhape_command.py 461 passed in 33.77s
tests/test_dry_run_asks_remotes.py 32 passed in 13.10s
tests/test_readme_windows_quoting_sentence.py 44 passed in 16.49s
tests/test_windows_paths.py 46 passed in 3.76s
tests/test_release_workflow.py 56 passed in 3.74s
tests/test_agents_md_codes.py 47 passed in 2.01s
tests/test_cli_reference.py 23 passed in 57.69s
tests/test_update_add_codes_documented.py 6 passed in 0.48s
tests/test_repo_hygiene.py 100 passed in 2.77s
tests/test_no_auto_maintenance.py 59 passed, 1 skipped in 9.83s

Main moved after the base (to 04923f10d), so I also built the merge of this head with it as a local, unpushed commit and re-ran the setup-facing files on that tree: tests/test_setup_project_py.py 286 passed in 107.82s; tests/test_setup_project_dry_run_offers.py 13 passed in 1.70s; tests/test_github_limits_init_check_org.py 94 passed in 27.81s; tests/test_github_name_limits.py 64 passed in 22.77s; tests/test_agents_md_codes.py 47 passed in 1.98s; tests/test_cli_reference.py 23 passed in 30.32s; tests/test_repo_hygiene.py 100 passed in 2.57s.

tests/test_shape_doctor.py was not run: shape-doctor.py imports nothing from setup-project.py, and this change touches neither.

Mutants: 23 written, 23 killed

M1 any helper counts as an answer; M2 the XDG file not read; M3 an empty XDG_CONFIG_HOME counts as set; M4 no ~/.config fallback; M5 --file= not read; M6 --file <path> not read; M7 a path the shell would change is taken; M8 a relative path is taken; M9 any scheme; M10 no password needed; M11 host not decoded; M12 only / ends the host; M13 last @ instead of first; M14 blanks at the ends kept; M15 an absent file counts as unreadable; M16 an unreadable file counts as empty; M17 a file named twice; M18 a helper beside store ignored; M19 the re-check inverted; M20 no re-check; M21 a dry run offers; M22 empty helper lines counted; M23 the scoped key not read.

Each is killed by one or more of the rows above (for example M1 by ..._with_no_github_login_is_warned_about, the dry-run row and the ..._asked_about_again_after_setup_git row; M9, M10 and M11 to M14 by ..._read_as_git_reads_it; M7, M8 and M16 by ..._whose_logins_this_cannot_read_is_left_alone; M22 by ..._holding_only_an_empty_line_is_no_credential_helper).

Merges

git merge-tree --write-tree of b2dbc8b2 with origin/main 04923f10d: clean (tree f03b66eb225db7a39b3845090683f4c5044e34bd). Also clean with each of the 13 open PRs: #1006, #1002, #988, #986, #983, #976, #975, #974, #973, #970, #958, #956, #954. None touches setup-project.py or tests/test_setup_project_py.py; #975 and #970 touch docs/cli.md, which this branch does not change.

Known NIT

One docstring line in _check_credential_helper (setup-project.py line 1416) is 106 characters, a rewrap miss. The repository sets no line-length rule and no test failed. It is left as it is so the gated head stays as it is.

Lane: openreposhape-3 (openRepoShape-3)

🤖 Generated with Claude Code

Summary by Sourcery

Warn about empty GitHub store credentials and offer gh auth setup-git before scaffold HTTPS pushes fail.

New Features:

  • Detect empty Git store credential files for GitHub and warn users before HTTPS pushes can fail.
  • Offer gh auth setup-git for machines whose configured helpers are exclusively empty or GitHub-login-free store helpers, including a recheck after setup.

Bug Fixes:

  • Stop treating any configured credential helper as proof that Git can authenticate to github.com.
  • Prevent scaffold pushes from failing after repository creation when an empty store credential helper cannot provide credentials.

Enhancements:

  • Read supported store credential locations and GitHub credential URL formats without invoking configured helpers or exposing stored credentials.
  • Preserve existing behavior for non-store helpers, mixed helpers, unreadable or unsupported credential files, dry runs, and preflight exit status.

Tests:

  • Add coverage for empty stores, supported credential locations and file options, Git-compatible URL parsing, unreadable or unsupported helpers, dry runs, and post-setup rechecks.

…thub.com gets the preflight's `[??]` and its `gh auth setup-git` offer, as a machine with no helper does, and the check no longer says every configured helper answers git's question for github.com (#992)

A configured helper is not a stored login. The check counted any helper
as an answer, and its docstring said `osxkeychain`, Windows' `manager`,
`store` and a devcontainer's own "all answer git's question for
github.com". `store` answers only with a login stored in it: over an
empty `~/.git-credentials` it answers nothing, and with its prompt off
git could not read a username (#992's probe). Every push the scaffold
makes runs with that prompt off since #971 (#915), so that machine's
first push fails after the three repositories exist (RV971 measured it).

Where every helper the two keys hold is `store`, the check now reads the
files git's `store` reads (`~/.git-credentials`, then
`$XDG_CONFIG_HOME/git/credentials` or `~/.config/git/credentials`, or a
`--file` that is an absolute path `sh` reads as itself) for a login git
would give https://github.com, read as git reads it and measured against
git's own `store`. None there, and it says so with the same `[??]` and
offer. Any other helper, a mix, a path it cannot read as git would, or a
file it cannot read is left alone. No helper is ever asked: `git
credential fill` runs every helper configured. The warning stays out of
the `checked` list, so `--preflight` exits 0 either way, and a dry run
still warns and offers nothing (#857).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR narrows credential preflight from “any helper is configured” to detecting whether an exclusively store-based configuration contains a readable GitHub HTTPS login, warning and offering gh auth setup-git only when the first push is expected to fail, with extensive compatibility and regression tests.

Sequence diagram for empty store credential preflight

sequenceDiagram
    participant Preflight
    participant GitConfig
    participant StoreFiles
    participant User
    participant gh

    Preflight->>GitConfig: _credential_helpers()
    GitConfig-->>Preflight: store helper configuration
    Preflight->>StoreFiles: _store_files(helper)
    StoreFiles-->>Preflight: credential file paths
    Preflight->>StoreFiles: _holds_a_github_login(path)
    StoreFiles-->>Preflight: no readable GitHub login
    Preflight->>User: warn(...)
    Preflight->>User: offer(gh auth setup-git)
    User-->>Preflight: accepts offer
    Preflight->>gh: gh auth setup-git
    gh-->>Preflight: success
    Preflight->>GitConfig: _why_no_login()
    GitConfig-->>Preflight: configured gh helper now answers
Loading

Flow diagram for credential preflight login detection

flowchart TD
    A[Preflight checks credential helpers] --> B["_why_no_login()"]
    B --> C{Credential helpers configured?}
    C -->|No| D[Warn: no helper; offer gh auth setup-git]
    C -->|Yes| E{Every helper is readable store configuration?}
    E -->|No| F[Leave configuration alone]
    E -->|Yes| G[Read store credential files]
    G --> H{Readable GitHub HTTPS login found?}
    H -->|Yes| F
    H -->|No| I[Warn: no stored GitHub login; offer gh auth setup-git]
    D --> J[Run gh auth setup-git]
    I --> J
    J --> K{"_why_no_login() after setup"}
    K -->|No warning| L[Report git will ask gh for credentials]
    K -->|Still warning| M[Leave warning state]
Loading

File-Level Changes

Change Details Files
Replace the configured-helper presence check with a conservative credential-availability check that only inspects readable store credential files and preserves existing behavior for helpers it cannot safely evaluate.
  • Collect and normalize helpers from the existing scoped and global Git config keys.
  • Resolve supported store forms, including default credential locations and safe absolute --file paths.
  • Parse credential-store lines for HTTPS GitHub logins using Git-like URL rules and ignore unreadable or non-store configurations.
  • Emit the existing warning and gh auth setup-git offer only when no readable store login is found, then re-check after setup.
  • Keep warnings non-refusing, dry-run behavior unchanged, and avoid invoking git credential fill.
setup-project.py
Add focused coverage for empty stores, credential-file resolution, Git-compatible parsing, unsupported configurations, dry runs, and post-setup rechecks.
  • Verify empty or GitHub-inapplicable store entries produce the warning while stored GitHub credentials suppress it.
  • Cover XDG and fallback credential locations, explicit file arguments, duplicate helpers, unreadable files, relative or shell-expanded paths, and mixed helper configurations.
  • Cross-check URL matching behavior against Git's own credential-store implementation.
  • Verify dry runs do not offer setup and successful or ineffective setup commands trigger the intended re-check.
  • Update the existing non-gh helper test documentation to reflect that configured helpers are not treated as readable logins.
tests/test_setup_project_py.py

Assessment against linked issues

Issue Objective Addressed Explanation
#992 Correct the documentation and test description so they distinguish a configured credential helper from a stored login, and limit claims about helper behavior to store. ✅
#992 Warn and offer gh auth setup-git when no helper is configured or when the configured helpers are all readable store helpers with no GitHub login, without invoking helpers to probe them. ✅
#992 Keep this check as a warning rather than a refusal: preserve preflight success and dry-run behavior, and re-check after offering setup. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

@brettheap

Copy link
Copy Markdown
Contributor Author

RV1008 review of #1008 (closes #992) at head b2dbc8b: READY. No MUST.

Measured at main 3864b18 and the head, with --preflight run from a scratch folder that is not the checkout, inside env -i. The stub gh answers only --version and auth status and exits 97 on anything else. GH_TOKEN=bogus, empty GH_CONFIG_DIR, GIT_SSH_COMMAND=false, system and global config and HOME in scratch, no remote. The exit code was 0 at both in every case.

  • store over an empty file, no file, or only a gitlab login; store --file=<abs> absent; store at system or at local scope: main silent, head [??] naming the files store reads.
  • github.com login in ~/.git-credentials, $XDG_CONFIG_HOME/git/credentials or --file; cache; gh's helper over store: silent at both.
  • No helper: the same [??] line at both, byte for byte.
  • Premise: store over an empty file, git credential fill with the prompt off, exit 128 "could not read Username".
  • yes at a pty: setup-git runs, then "[ok] git will ask gh for github.com credentials".
  • Dry run (-- --dry-run) at a pty: exit 2 at both, at the same remote-unreadable refusal (stub gh). Steps (1)-(5) identical, gh calls identical, global config unchanged. The head adds only the [??] line and "not offered".

The four decisions are sound. Notes below on 1, 2 and 4.

SHOULD-1 (follow-up issue, does not block): setup-project.py:1380-1406. With url.git@github.com:.insteadOf https://github.com/ and store holding no github login, the head prints a false [??] and an offer where main was silent: git ls-remote --get-url https://github.com/demoorg/x.git gives git@github.com:demoorg/x.git, so no helper is asked. The no-helper warning on main has the same blindness. Fix both in a follow-up: ask git ls-remote --get-url (no network) and skip both warnings when the answer is not https://.
NIT-1: setup-project.py:1303. Decision 1 understates. With store in credential.helper and another helper under credential.https://github.com/.helper, git fill exits 0 with a login, yet the head warns where main was silent. Say so in decision 1, or carry it into SHOULD-1's follow-up.
NIT-2: setup-project.py:1368 and :1390. "a file this cannot read, may answer" is false. git's store treats a mode-000 file as empty (credential-store get exits 0 with no password, fill exits 128) and dies on a folder. Keep the silence and reword the docstring.
NIT-3 (the known one): setup-project.py:1416 is a 106-character docstring line. Ruling: a NIT for a follow-up. Do it with NIT-2's rewording, or in any fix round this PR gets for another reason. Not worth a re-gate alone.
NIT-4: tests/test_setup_project_py.py:1578-1588. Every credential row mocks run, so a mutant that reads helpers with --global only survives the whole file (286 passed). This predates the PR. Add one real-git row with GIT_CONFIG_SYSTEM holding credential.helper=store.
NIT-5: setup-project.py:1326. With HOME unset, git cannot expand ~ (exit 128), so git's store reads no ~ file but the check reads the passwd home. Ask git: git -c <k>='~/.git-credentials' config --type=path --get <k>, which also covers Git for Windows' choice of HOME.

Tests at the head, one at a time:

  • test_setup_project_py.py: 286 passed in 98.01s
  • test_setup_project_dry_run_offers.py: 13 passed in 1.68s
  • test_setup_sh.py: 129 passed in 71.64s
  • test_agents_md_codes.py: 47 passed in 1.95s
  • test_update_add_codes_documented.py: 6 passed in 0.40s
  • test_repo_hygiene.py: 100 passed in 2.51s
  • test_cli_reference.py: 23 passed in 29.78s
  • test_no_auto_maintenance.py: 59 passed, 1 skipped in 11.54s
    Skipped: the other 20 files of F992's gate (results in the PR body), and test_shape_doctor.py.

Mutants: the writer's M1, M13 and M16 remade and killed. Mine: a remedy naming gh auth login, killed by 9 rows; the re-check asking only whether a helper is configured, killed by ..._asked_about_again_after_setup_git[False]; helpers read with --global only, which survives (NIT-4).

Merges: clean with origin/main 3864b18, clean with #1011 f090a3c, and clean three-way (main with #1011, then the head). test_setup_project_py.py on the three-way tree: 286 passed. No other open PR touches setup-project.py or tests/test_setup_project_py.py.

SonarCloud: 0 open issues. CI tests was still pending at 2026-10-19T21:47Z: land only once it is green.

Lane: openreposhape-3 (openRepoShape-3)

@sourcery-ai

sourcery-ai Bot commented Oct 19, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 20 hours and 27 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

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

Labels

None yet

Projects

None yet

1 participant