Skip to content

adopt-project.py: _elector and _remote_repository read git config user.name and the origin URL through git_out, which decodes strictly, so a name or URL that is not UTF-8 ends plan (and, for the URL, check and execute) in a UnicodeDecodeError traceback, exit 1, naming neither the git setting nor the flag that avoids it #292

Closed

Description

@brettheap

(lane openRepoShape-2, 2026-10-12T00:11:27Z)

Found by this lane's adversarial review of PR #270 at aed9190 (the fix for #255), its follow-up FU1. It is the same on main d94d155, where it was reproduced again today, and at #270's head and its merge with main: the PR neither causes it nor changes it. Nothing is created in any of the runs below. Not claimed.

What is wrong. adopt-project.py asks git two questions whose answer is text it did not write, and reads both with repo_shape.git_out, which decodes the output with .decode("utf-8"). _elector asks git config user.name when --elected-by is not given; _remote_repository asks git remote get-url origin when it opens a local source. Neither catches UnicodeDecodeError: it is a ValueError, not a Refusal, so main() (:3703-3710) does not catch it either, and the run ends in a traceback, exit 1. A refusal would say what to do (pass --elected-by, pass --org); the traceback names neither the setting nor the flag.

# $U: a checkout of openRepoShape at d94d155. $SRC: a three-file git repository (src/app.py, docs/spec.md, README.md), one commit, no origin. Local; no network.
printf '[user]\n\tname = Jos\xe9\n' > "$G"                       # a global config whose user.name holds a Latin-1 byte
GIT_CONFIG_GLOBAL="$G" GIT_CONFIG_NOSYSTEM=1 python3 "$U/adopt-project.py" plan \
    --source "$SRC" --project Northwind --org testorg --elected-on 2026-09-02 --out "$OUT/plan.yaml"
#   exit 1, no plan file
#   adopt-project.py line 1512, in cmd_plan / line 1419, in _checked_plan_inputs / line 3615, in _elector
#   scripts/repo_shape.py line 777, in git_out:   return proc.stdout if binary else proc.stdout.decode("utf-8").strip()
#   UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 3: invalid continuation byte
git -C "$SRC" remote add origin "$TMP/caf"$'\xe9'"/repo.git"       # an origin URL that is a non-UTF-8 path
python3 "$U/adopt-project.py" plan --source "$SRC" --project Northwind --org testorg \
    --elected-by 'T H' --elected-on 2026-09-02 --out "$OUT/plan2.yaml"
#   exit 1, no plan file
#   adopt-project.py line 1508, in cmd_plan / line 564, in open / line 686, in _remote_repository / repo_shape.py line 777, in git_out
#   UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 137: invalid continuation byte
the input the run result on d94d155
global user.name = Jos\xe9, no --elected-by plan exit 1, UnicodeDecodeError from _elector (:3615), no plan written
global user.name = Jose (control) plan exit 0, elected_by: Jose
the same Latin-1 user.name, --elected-by 'T H' given (control) plan exit 0: _elector is not called
origin URL holding \xe9, a plan written earlier check exit 1, UnicodeDecodeError via open_source (:1672), Source.open (:564), _remote_repository (:686)
the same execute --yes --local-remote-dir ... exit 1, the same traceback, before any leg exists (no leg repository under the local remote dir)
origin URL holding \xe9, no plan yet plan exit 1, the same traceback, no plan written

Why. git_out (scripts/repo_shape.py:765-777) returns proc.stdout.decode("utf-8").strip() unless binary=True, and raises Refusal only for a git exit code. The two callers wrap it in except Refusal (_elector, adopt-project.py:3613-3618; _remote_repository, :684-691, which returns None on one, so that a source with no origin reads as no org/repo). A byte that is not UTF-8 reaches neither handler. Both values are bytes git stores without interpretation (a config value, a URL), so a repository can hold one without anyone having done anything wrong: a name saved in a legacy code page, a clone path under a non-UTF-8 directory.

What it should do.

  1. _remote_repository treats an origin it cannot decode as no org/repo, returning None as it does for a missing origin, so plan meets its existing adopt-no-org refusal ("pass --org") or takes --org, and check and execute read the source's repository as unknown. Source.open then no longer dies for a source nobody can adopt by name.
  2. _elector refuses by name. A user.name that is not UTF-8 is a refusal that says so and says to pass --elected-by (exit 2), before the plan is written. It should not decode with errors="surrogateescape" and carry on: the lone surrogate would then be the elector the plan records, which is adopt-project.py: since #247 a byte that is not UTF-8 in --elected-by, --reference or the --source directory passes plan and check, and execute pushes both legs before it dies with UnicodeEncodeError in write_lf #255's input, and execute would push both legs before it dies writing it.
  3. Mind where the fix goes. git_out lives in scripts/repo_shape.py, a pinned copy (COPIED_FROM_SHAPE): a change there moves the sha256 that every scaffolded and adopted project's contracts/shape-pin.yaml records for it, and every project already cut sees it as upstream-changed. The two call sites are in adopt-project.py and can be fixed without it (catch UnicodeDecodeError beside Refusal, or call with binary=True and decode there). Whether other git_out callers need the same is a separate question.

How to test. In tests/test_adopt_plan.py, with GIT_CONFIG_GLOBAL pointing at a config written with write_bytes (tests/test_scaffold_ignored_shape_files.py:64-77 does the same for excludes):

  • (a) a user.name holding b"Jos\xe9" and no --elected-by: exit 2, a refusal that names user.name and --elected-by, no Traceback, no plan file;
  • (b) a source whose origin URL holds \xe9, with --org: plan exits 0 and records no repository from it; without --org, plan exits 2 with adopt-no-org; check on a plan written beforehand exits 0 or 2 with no Traceback;
  • (c) controls: an ASCII user.name, and --elected-by given with the Latin-1 config.

Reverting either guard fails its own case. (a) and (c) run on every OS; (b) passes the URL to git remote add as bytes, so it is POSIX only and skipped on Windows.

Related: #255 and PR #270 (a non-UTF-8 byte in --elected-by, --reference or the source path, which #270 refuses as adopt-unwritable-value; this issue is the same byte arriving from git and not from argv, so it never reaches that check); #261 (blank or malformed --elected-by, which the empty answer from a missing user.name also meets); #192 (closed: the same crash in load_yaml, fixed there).

🤖 Generated with Claude Code

Activity

  1. brettheap commented on Oct 12, 2026

    @brettheap
    ContributorAuthor

    CLAIMED — lane openRepoShape-2, session 5b7b5f3d-dce7-452d-a33d-ab8537d91db3@Eagle, 2026-10-12T11:00:39Z, for #292

    Logged in lanes/log/openRepoShape-2.md at f6ba56a14f4e519a6885fc1258972a8f04e445df.

    The three reads (lane-collision-protocol Rule 1):

    1. existing `CLAIMED —` comments on opensoft/openRepoShape#292:
       none
    2. `gh pr list --repo opensoft/openRepoShape --state all --search "#292"`, kept where the row or its body names `#292`:
       none
    3. `git ls-remote --heads git@github.com:opensoft/openRepoShape` branches naming `292` as a whole token:
       none
    
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