Skip to content

[BUG] git/gh subprocess text is locale-dependent; pin the C locale via iris.shell.git_env() - #239

Open
rafaeldearaujop wants to merge 1 commit into
mainfrom
fix/git-subprocess-locale
Open

[BUG] git/gh subprocess text is locale-dependent; pin the C locale via iris.shell.git_env()#239
rafaeldearaujop wants to merge 1 commit into
mainfrom
fix/git-subprocess-locale

Conversation

@rafaeldearaujop

Copy link
Copy Markdown

[BUG] git/gh subprocess text is locale-dependent; pin the C locale via iris.shell.git_env()

Aberta como PR direta, não como issue: bug reproduzível com fix validado
em mãos; o corpo segue o template bug.yml para rastreabilidade.
Referências arquivo:linha apontam para main @ da16235.

Summary

O engine repassa o stderr do git em mensagens de erro, e o git localiza
esse texto conforme LANG/LC_MESSAGES. Em máquinas pt_BR.UTF-8, a
mensagem para caminho inexistente é "Arquivo ou diretório inexistente" — e
tests/test_git_reader.py::test_nonexistent_path_raises_with_git_stderr
(que espera "no such file or directory") falha. A CI passa porque roda em
locale inglês. Esta PR fixa o locale C em todos os subprocessos de git
e gh do engine, sem alterar o ambiente do usuário.

Reproduction steps

  1. LANG=pt_BR.UTF-8 LC_ALL=pt_BR.UTF-8 pytest tests/test_git_reader.py -q
    1 failed em test_nonexistent_path_raises_with_git_stderr
    (tests/test_git_reader.py:66).
  2. LC_ALL=C pytest tests/test_git_reader.py -qpassed.
  3. Causa direta: git -C /caminho/inexistente log -1
    fatal: cannot change to '/caminho/inexistente': Arquivo ou diretório inexistente
    (com LANG=C: … No such file or directory).

Expected behaviour

Mensagens e parsing de saída do git/gh estáveis por locale: o mesmo
comando, na mesma versão, produz o mesmo texto em qualquer máquina. A suíte
passa localmente em qualquer locale.

Actual behaviour

  • O teste falha em locale não-inglês (reproduzido em pt_BR.UTF-8, git 2.43.0).
  • Mais amplo que o teste: qualquer ponto que dependa de texto do git é
    frágil por locale. O parsing estrutural (--pretty com separadores,
    --numstat, --porcelain, --json) já era locale-safe; as mensagens de
    erro que chegam ao usuário e aos testes não eram.

Surface

Engine / CLI (Python)

Versão do CLI

v1.6.1 (main @ da16235)

Repo analisado

Não se aplica — reproduz com caminho inexistente.

Logs / screenshots / trace-id

$ LANG=pt_BR.UTF-8 git -C /definitivamente/nao/existe log -1
fatal: cannot change to '/definitivamente/nao/existe': Arquivo ou diretório inexistente
$ LANG=C git -C /definitivamente/nao/existe log -1
fatal: cannot change to '/definitivamente/nao/existe': No such file or directory

Correção aplicada

iris/shell.py (novo, 25 linhas): git_env() -> dict[str, str] devolve
os.environ com LC_ALL=C e LANG=C. É função, não constante de
módulo
, para refletir os.environ no momento da chamada (testes fazem
monkeypatch.setenv) em vez de congelar um snapshot na importação.

23 call sites em 11 arquivos passam env=git_env() — exatamente os
subprocess.run cujo argv começa com "git" ou "gh":

Arquivo Call sites O que roda
iris/ingestion/git_reader.py :71, :98, :132 git log, git rev-parse
iris/ingestion/diff_reader.py :75 git show
iris/ingestion/github_reader.py :37, :232, :288, :390 git remote, gh api graphql, gh pr list, gh pr view
iris/analysis/durability.py :250 git blame
iris/analysis/code_provenance.py :193 git blame
iris/analysis/repo_kind.py :143 git ls-files
iris/analysis/priming_detector.py :139 git log --diff-filter=A
iris/cli.py :243, :942, :982, :1168, :1180 git remote get-url, gh pr view, gh pr comment, git check-ignore, git rev-parse
iris/platform/identity.py :33, :61, :89 gh api
iris/platform/config.py :66, :78 gh api user, git config
iris/hooks/manager.py :127 git *args (sonda de configuração)

Deliberadamente não tocados, e por quê:

  • iris/hooks/manager.py:322 — executa o arquivo de hook, não o git; o
    env ali já carrega a testemunha da sonda de alcance.
  • iris/cli.py:1640curl … | sh do auto-upgrade; não é git/gh.
  • iris/update/auto.py:186Popen do updater; idem.
  • iris/ingestion/external_reader.py — comandos fornecidos pelo usuário
    com env próprio; impor locale seria alterar o comando dele.

Guard estático (tests/test_subprocess_locale.py): varre iris/**/*.py
e falha se algum subprocess.run( cujo argv literal começa com git/gh
não passa env=. Limitação conhecida: um call site
(github_reader.py:232) recebe args como variável, não literal — foi
corrigido à mão, mas o guard não o enxergaria se regredisse. Alternativa
(resolver o nome args até o literal) foi julgada complexidade demais para
um caso; a limitação está na docstring do teste.

Validação (isolada: main + só esta mudança)

Antes Depois
pytest tests/ -q sob LC_ALL=C 473 pass 476 pass
pytest tests/ -q sob LC_ALL=pt_BR.UTF-8 472 pass / 1 fail 476 pass / 0 fail
tests/test_subprocess_locale.py 3/3 (regressão sob pt_BR · isolamento do os.environ · guard)
check_analysis_chain.py 22/15 22/15
A/B E2E em par, sob pt_BR, engine base vs esta mudança metrics.json e report.md byte-idênticos
python -m compileall iris ok

O E2E idêntico é o resultado esperado e importante: a saída estruturada já
era locale-safe, então fixar o locale não pode mudar nenhum número — só
torna as mensagens de erro determinísticas.

Como reproduzir

LC_ALL=pt_BR.UTF-8 LANG=pt_BR.UTF-8 pytest tests/ -q   # 476 passed (em main: 1 failed)
LC_ALL=C pytest tests/ -q                                 # 476 passed
python scripts/check_analysis_chain.py

Análise adversária — toda linha com disposição final

# Ataque Resposta Disposição
1 É só um teste; o engine funciona. O parsing estrutural é locale-safe, sim — mas a mensagem que o usuário vê num erro era localizada, e a suíte não rodava em máquinas brasileiras sem LC_ALL=C. ✅ Corrigido; custo: 23 linhas de env= + um módulo de 25 linhas.
2 LC_ALL=C pode quebrar caminhos não-ASCII na saída do git. Git escapa caminhos não-ASCII por padrão (core.quotePath) independentemente do locale; repo_kind.py já usa -z e errors="surrogateescape". Não piora. E2E byte-idêntico confirma. ✅ Mitigado (medido).
3 Por que não corrigir só o teste (aceitar ambas as strings)? Corrigiria o sintoma e deixaria a fragilidade. CLAUDE.md, Coding Principles: "Favor deterministic outputs". ✅ Decidido: corrigir a fonte.
4 gh não localiza mensagens; por que incluí-lo? Uniformidade: "todo subprocesso git/gh roda em locale C" é uma regra que se lê e se verifica; "só os que hoje importam" exige julgar caso a caso e regride em silêncio. E gh repassa erros do git em alguns caminhos. ✅ Decidido: incluir, sob o guard.
5 Constante de módulo seria mais simples que função. Constante congela os.environ na importação; testes com monkeypatch.setenv e processos longos veriam um ambiente stale. ✅ Decidido: função. Testado em test_git_env_pins_c_locale_and_keeps_the_rest.
6 O guard tem um ponto cego (args como variável). Um call site, corrigido à mão e documentado. Estender o guard para resolver nomes exigiria análise de fluxo por um caso. ✅ Mitigado; limitação escrita na docstring do teste.
7 Um helper novo contradiz "explicit over abstraction"? 23 env={**os.environ, "LC_ALL": "C", "LANG": "C"} repetidos seria pior para ler e para manter do que uma função de três linhas com o porquê na docstring. ✅ Decidido: helper mínimo, sem wrapper de subprocess.run.

Fora do escopo

  • Traduzir mensagens de erro do Iris (o i18n do relatório já cobre o que é do Iris; isto é o texto do git).
  • Comandos externos definidos pelo usuário (external_reader.py).

Issues relacionadas (verificado contra as 63 issues do repo, abertas e fechadas — sem duplicata)

Referências

  • Novo: iris/shell.py; tests/test_subprocess_locale.py.
  • Call sites (main @ da16235): iris/ingestion/git_reader.py:71,98,132; iris/ingestion/diff_reader.py:75; iris/ingestion/github_reader.py:37,232,288,390; iris/analysis/durability.py:250; iris/analysis/code_provenance.py:193; iris/analysis/repo_kind.py:143; iris/analysis/priming_detector.py:139; iris/cli.py:243,942,982,1168,1180; iris/platform/identity.py:33,61,89; iris/platform/config.py:66,78; iris/hooks/manager.py:127.
  • Não tocados: iris/hooks/manager.py:322; iris/cli.py:1640; iris/update/auto.py:186; iris/ingestion/external_reader.py.
  • tests/test_git_reader.py:61-66; CLAUDE.md → Coding Principles ("Favor deterministic outputs").

…git_env()

Git localizes its messages; on pt_BR machines the stderr surfaced in RuntimeError read 'Arquivo ou diretório inexistente' and test_git_reader failed while CI passed. Add iris/shell.py::git_env() (a function, so it reflects os.environ at call time) and pass env=git_env() at the 23 subprocess.run sites whose argv starts with git or gh. Structured output was already locale-safe: paired E2E under pt_BR is byte-identical. tests/test_subprocess_locale.py adds the pt_BR regression, an env-isolation check and a static guard.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@rafaeldearaujop rafaeldearaujop added the type: bug Algo funciona incorretamente, quebra, ou regrediu label Sep 8, 2026
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clickbus-iris Ready Ready Preview Sep 8, 2026 11:41pm UTC

Request Review

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

Labels

type: bug Algo funciona incorretamente, quebra, ou regrediu

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant