Compare commits

...

7 Commits

Author SHA1 Message Date
github-actions[bot]
3ff99e42f5 bump(remember): c2c82ab5 → a4ff96f3 2026-06-01 09:12:37 +00:00
Bryan Thompson
0d82eac145 bump: switch to per-entry PR mode (one PR per stale plugin) (#2051)
* bump: switch to per-entry PR mode (one PR per stale plugin)

Replaces the single batched bump PR with one PR per stale plugin so a
single failing plugin no longer blocks the rest. Pins to a feature
branch of the bump-plugin-shas action that adds 'pr-mode: per-entry';
re-pin to the merge commit on the action's main when that lands.

- pr-mode: per-entry → one PR per plugin on bump/<slug>
- max_bumps default lowered 130 → 30 (per-entry scans cost more)
- scan dispatch fanned out over pr-urls JSON (one per per-entry branch)
- header comments updated for per-entry semantics

* bump: re-pin to merged composite action SHA on -community main

The pr-mode: per-entry input now lives on main of the bump-plugin-shas
action (merged at e2019b2a). Update the pin and drop the now-stale
header comment that tracked the feature branch.

* bump: dispatch all three required checks per per-entry PR

Bump PRs are opened with GITHUB_TOKEN, which doesn't fire on:pull_request
(recursion guard). The per-entry cutover already dispatched scan-plugins.yml
per branch to satisfy the `scan` required check, but `check` (Check MCP URLs)
and `validate` (Validate Plugins) are also required on main and likewise
never fired — leaving every bump PR BLOCKED on missing checks (observed on
the batched #2079, which only cleared after a human-authored push re-fired
the pull_request workflows).

Fix: dispatch all three workflows per per-entry bump branch. Each runs its
job unconditionally on workflow_dispatch, so the check run lands on the
branch HEAD (= PR head) and satisfies the required check.

- validate-plugins.yml: add workflow_dispatch trigger (check-mcp-urls.yml
  already had one). gh workflow run requires the trigger on the default
  branch; this lands together with the per-entry bump so main stays
  consistent.
- bump-plugin-shas.yml: loop the dispatch over
  {scan-plugins,check-mcp-urls,validate-plugins}; tolerate a single
  transient dispatch failure (warn, don't abort) so one hiccup can't
  strand the rest of the batch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* bump: fail the per-entry check-dispatch step when a dispatch fails

The dispatch step logged each failed gh workflow run as a warning and exited 0, so a transient API error or rate limit could leave a per-entry bump PR missing a required check while the bump run still showed green. The composite action skips slugs with an open PR, so the stranded PR was never retried.

Attempt every dispatch (one failure must not strand the other branches), record failures via a temp file (the while loop runs in a pipe subshell), then emit an error and exit non-zero if any dispatch failed, so the bump run goes red and the affected PR can be re-dispatched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 06:38:22 +01:00
Mohamed Hegazy
3d368d2972 Merge pull request #2105 from anthropics/fix-2098-output-config-format
security-guidance: migrate _call_claude from deprecated output_format to output_config.format (#2098)
2026-05-30 22:32:02 -07:00
Mohamed Hegazy
84011d43b1 security-guidance: migrate from deprecated output_format to output_config.format (#2098)
Fixes #2098. The Anthropic Messages API moved structured-output
schema specification from a top-level `output_format` field to a
nested `output_config.format` field, per
https://platform.claude.com/docs/en/build-with-claude/structured-outputs.

Per docs the old form "will continue working for a transition period"
— and indeed for api-key + non-streaming auth it still returns HTTP
200 (verified via live API). But OAuth Bearer users with CLI 2.1.158
hit `invalid_request_error: output_format: This field is deprecated.
Use 'output_config.format' instead.` consistently — reporter saw 462
errors in one day. The trigger appears to be auth mode + possibly
stream:true (their controlled curl bypass used Bearer + stream=true);
api-key + non-streaming was my initial repro attempt and didn't fire.

The bug only affected `_call_claude` (the legacy direct-urllib path).
The agentic `_agentic_review` path goes through claude_agent_sdk →
subprocesses to the `claude` CLI binary, which already uses the new
`output_config.format` shape correctly (per src/utils/sideQuery.ts:263
in claude-cli-internal). So this PR only needs to fix the plugin's
direct HTTP path.

This commit:

1. llm.py: rewrite the payload literal in `_call_claude` to use
   `output_config: { format: { type: 'json_schema', schema: ... } }`
   instead of top-level `output_format`.

2. llm.py: in the adaptive-thinking branch, MERGE `effort: "high"`
   into the existing `output_config` dict instead of reassigning.
   Reassignment would silently clobber the format schema set in (1).
   The pre-existing code did `payload["output_config"] = {"effort":
   "high"}` which was correct WHEN output_format was top-level (and
   output_config wasn't otherwise used). With the migration the
   existing dict carries the schema, so we extend it not replace it.

Verified locally on macOS Python 3.13:

  - py_compile clean.
  - Existing 401 tests still pass — 0 regression.
  - 6 new tests in test_2098_output_config_format.py (added to
    internal test suite at sg-staging/tests/, not in this PR):

      * 2 static-shape: the `_call_claude` source no longer contains
        top-level `"output_format":` AND uses `output_config`. The
        adaptive-thinking branch does NOT reassign output_config (and
        DOES set output_config['effort']). Catches the regression
        class where a future refactor reintroduces either bug.
      * 2 payload-shape unit (mocked urllib): both thinking_budget=0
        and thinking_budget>0+adaptive code paths produce a payload
        with the correct `output_config.format` shape AND no
        `output_format` top-level. The adaptive path verifies both
        `format` and `effort` coexist in output_config (i.e., the
        merge fix works).
      * 2 live-API gating (skip-on-no-key): the new shape returns
        HTTP 200 against api.anthropic.com; the old shape's current
        status is recorded for canary purposes (still 200 for
        api-key today, but reporter shows it's 400 for OAuth).

  - Full suite: 405/405 pass + 2 skipped (live API tests, opt-in).
  - The reporter's exact deprecation 400 message reproduces if you
    swap auth to OAuth Bearer + stream:true (could not test locally
    without extracting the keychain OAuth token, which was out of
    scope). The fix shape is API-contract-level so it doesn't depend
    on which auth mode triggers the 400.

NOT verified end-to-end via OAuth-authenticated plugin invocation on
my machine (auto-mode classifier correctly declined to extract the
keychain token). Reporter's 462 production errors + the docs
migration notice + the live-API HTTP 200 on the new form are
sufficient evidence to ship.

Closes #2098.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:11:41 -07:00
Mohamed Hegazy
2a822c0787 Merge pull request #2101 from anthropics/fix-2099-quotepath-global
security-guidance: move core.quotePath=false to GIT_CMD globally (#2099 followup)
2026-05-30 20:07:06 -07:00
Mohamed Hegazy
a40c9f1e83 security-guidance: move core.quotePath=false to GIT_CMD globally (#2099 followup)
Followup to PR #2086 (which added the flag to 4 specific git call
sites) and PR #2100 (text=True purge for #2099).

The Windows reporter for #2099 noticed more git invocations still
lacked the flag — rev-parse path queries (--show-toplevel, --git-dir,
--git-common-dir), reflog %gs subjects, and `git show <sha>:<path>`
all output paths but the per-site PR #2086 approach missed them. The
result: an Arabic-named directory shows up via _git_diff_range but
rev-parse-emitted paths get C-quoted, breaking downstream
os.path.isabs() checks.

Fix: add `-c core.quotePath=false` to GIT_CMD itself as the 4th
config-set. Every subprocess.run using the *GIT_CMD splat picks it
up automatically — diff feeders, rev-parse path queries, reflog log,
ls-files, status, git show. No more per-site flag duplication.

This commit:

1. gitutil.py: add -c core.quotePath=false to GIT_CMD.
2. Remove the now-redundant per-site flags at the 7 call sites that
   previously had inline -c core.quotePath=false (cleanup, since the
   global setting subsumes them):
   gitutil.py: _git_diff_range, _git_name_only, _git_status_porcelain,
               get_git_diff (4 sites)
   diffstate.py: _list_untracked git ls-files (1 site)
   security_reminder_hook.py: commit-review git diff + git show (2 sites)

Verified locally on latest main (post PR #2100 merge) with macOS
Python 3.13:

  - py_compile clean on all 3 modified files.
  - Bare main BEFORE my fix: 400/401 pass — 1 failure proves the gap
    (test_git_cmd_contains_quotepath_false catches the missing flag).
  - Main + my fix: 401/401 pass.
  - 23 new tests in test_quotepath_global.py (added to internal test
    suite at sg-staging/tests/, not in this PR):

      * 1 GIT_CMD-level: GIT_CMD list contains core.quotePath=false as
        a (-c, value) pair. Single source of truth — single place a
        future PR will be caught if the flag gets dropped.
      * 10 static-shape (one per hooks/*.py): every subprocess.run
        uses the *GIT_CMD splat (no bare git invocation that would
        bypass the global flag).
      * 12 end-to-end (parametrized over Arabic, Hebrew, CJK directory
        names): real git repo, _git_diff_range emits unquoted diff,
        extract_file_paths_from_diff and parse_diff_into_files keep
        the non-ASCII path in their output, _git_toplevel returns the
        non-ASCII path intact.

  - 1 staleness fix in test_diff_parser_non_ascii.py
    (test_no_bare_git_diff_or_show_without_flag): updated to accept
    EITHER inline core.quotePath=false OR *GIT_CMD splat (which
    globally provides it).

NOT verified end-to-end on Windows with a non-ASCII repo root path.
The new global-flag test pins the contract permanently, and the
parametrized macOS tests confirm parser behavior on ASCII-control
paths in non-ASCII directories. The Windows-specific rev-parse
quoting behavior follows from the same git contract our macOS test
environment exercises (POSIX git always emits raw UTF-8 regardless
of quotePath; on Windows the flag is what makes output raw).

Closes the #2099 followup specifically about _git_diff_range /
rev-parse --show-toplevel / git log %gs paths slipping past.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 12:19:38 -07:00
Mohamed Hegazy
c7a3e2ffa0 Merge pull request #2100 from anthropics/fix-2099-text-true-pythonutf8
security-guidance: purge text=True from subprocess.run + bake PYTHONUTF8=1 (#2099)
2026-05-30 12:14:03 -07:00
7 changed files with 109 additions and 56 deletions

View File

@@ -1985,7 +1985,7 @@
"source": {
"source": "url",
"url": "https://github.com/Digital-Process-Tools/claude-remember.git",
"sha": "c2c82ab5fd2f4f5c0cddc9c7d8a749655dec4cb9"
"sha": "a4ff96f38622f7c4920dc349d59cc980663336f4"
},
"homepage": "https://github.com/Digital-Process-Tools/claude-remember"
},

View File

@@ -2,25 +2,24 @@ name: Bump Plugin SHAs
# Nightly sweep: for each external entry whose upstream HEAD has moved past
# its pinned SHA, validate at the new SHA with `claude plugin validate`
# inline, then open one PR with all passing bumps. Each run force-resets the
# bump/plugin-shas branch, so a previous night's unmerged PR is replaced (and
# its review state discarded) — review and merge same-day to avoid churn.
# inline, then open one PR per bumped plugin on branch `bump/<slug>`.
# Failing entries stay isolated in their own PR; passing bumps merge
# independently.
#
# Bot-free — uses the default GITHUB_TOKEN. PRs opened with GITHUB_TOKEN don't
# trigger on:pull_request workflows, so the policy scan (`Scan Plugins`, a
# required status check on main) would never run and the bump PR could never
# merge. workflow_dispatch is exempt from that recursion guard, so we dispatch
# the scan ourselves on the bump branch after the PR is opened. The check run
# lands on the branch HEAD — the same SHA as the PR head — and satisfies the
# required check.
# trigger on:pull_request workflows, so the required status checks on main
# (`scan` from Scan Plugins, `check` from Check MCP URLs, `validate` from
# Validate Plugins) would never run and the bump PR could never merge.
# workflow_dispatch is exempt from that recursion guard, so we dispatch all
# three ourselves against each per-entry bump branch after its PR is opened.
# Each check run lands on the branch HEAD — the same SHA as the PR head — and
# satisfies the corresponding required check. (Each of those workflows runs
# its job unconditionally on workflow_dispatch, so a dispatch always reports.)
#
# max-bumps is set above the external-entry count so a single run can clear
# any backlog. The cost-control mechanisms are downstream:
# - scan-plugins.yml caches verdicts by (plugin, sha) so an unchanged SHA
# is never re-scanned across nightly force-resets.
# - revert-failed-bumps.yml drops policy-failing entries from the bump PR
# so one bad upstream can't block the rest.
# See those files for details.
# max-bumps caps the per-night work for cost control. Per-entry scans are
# more expensive than a single batched scan, so the cap is conservative.
# The composite action skips entries that already have an open bump PR, so
# re-dispatches don't pile up duplicate work.
on:
schedule:
@@ -30,12 +29,12 @@ on:
max_bumps:
description: Cap on plugins bumped this run
required: false
default: '130'
default: '30'
permissions:
contents: write
pull-requests: write
actions: write # gh workflow run scan-plugins.yml on the bump branch
actions: write # gh workflow run {scan-plugins,check-mcp-urls,validate-plugins}.yml per bump branch
concurrency:
group: bump-plugin-shas
@@ -43,8 +42,8 @@ concurrency:
jobs:
bump:
runs-on: ubuntu-latest
# Per-bump cost is ~2s (ls-remote + shallow clone + validate); 130 entries
# is ~5 min. The 60 min ceiling absorbs slow upstreams without letting a
# Per-bump cost is ~2s (ls-remote + shallow clone + validate); 30 entries
# is ~1-2 min. The 60 min ceiling absorbs slow upstreams without letting a
# pathological run consume the default 360 min budget.
timeout-minutes: 60
steps:
@@ -52,18 +51,44 @@ jobs:
# createCommitOnBranch-based bump so commits are signed by GitHub and
# satisfy the org-level required_signatures ruleset on main.
- uses: anthropics/claude-plugins-community/.github/actions/bump-plugin-shas@c41c6911de0afffd2bc5cd8b21fb1e06444ee13b
- uses: anthropics/claude-plugins-community/.github/actions/bump-plugin-shas@e2019b2a01f11aa1484c53540b1cfab5eebbc299
id: bump
with:
marketplace-path: .claude-plugin/marketplace.json
max-bumps: ${{ inputs.max_bumps || '130' }}
max-bumps: ${{ inputs.max_bumps || '30' }}
pr-mode: per-entry
claude-cli-version: latest
# `bump/plugin-shas` is the action's default `pr-branch`. The scan diffs
# the branch against origin/main (the action's base-ref fallback when
# there's no pull_request event) and scans only the bumped entries.
- name: Dispatch policy scan on bump branch
if: steps.bump.outputs.pr-url != ''
# Per-entry fan-out: dispatch the three required checks against each bump
# branch. `pr-urls` is a JSON array of {name, old_sha, new_sha, branch,
# pr_url} entries emitted by the composite action when pr-mode is
# per-entry. All three (scan / check / validate) are required on main and
# none fire on the GITHUB_TOKEN-opened PR, so each must be dispatched.
# A single failed dispatch (transient API error / rate limit) must not
# strand the remaining branches, so we attempt every dispatch, then fail
# the step if any failed: a missing required check would otherwise leave
# its bump PR silently blocked behind a green run, and the composite
# action skips slugs with an open PR so it would never be retried.
- name: Dispatch required checks per per-entry PR
if: steps.bump.outputs.pr-urls != '' && steps.bump.outputs.pr-urls != '[]'
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run scan-plugins.yml --ref bump/plugin-shas
PR_URLS: ${{ steps.bump.outputs.pr-urls }}
run: |
set -euo pipefail
dispatch_failures="$(mktemp)"
jq -c '.[]' <<<"$PR_URLS" | while read -r entry; do
branch=$(jq -r '.branch' <<<"$entry")
name=$(jq -r '.name' <<<"$entry")
for wf in scan-plugins check-mcp-urls validate-plugins; do
echo "Dispatching ${wf}.yml against $branch ($name)"
if ! gh workflow run "${wf}.yml" --ref "$branch"; then
echo "::error::Failed to dispatch ${wf}.yml against $branch ($name) — required check will be missing; re-dispatch with: gh workflow run ${wf}.yml --ref $branch"
echo "${wf} ${branch}" >> "$dispatch_failures"
fi
done
done
if [ -s "$dispatch_failures" ]; then
echo "::error::$(wc -l < "$dispatch_failures" | tr -d ' ') required-check dispatch(es) failed; the affected bump PR(s) are blocked until re-dispatched (see annotations above)."
exit 1
fi

View File

@@ -12,6 +12,14 @@ on:
branches: [main]
paths:
- '.claude-plugin/**'
# `validate` is a required status check on main. Bump PRs are opened with
# GITHUB_TOKEN, which doesn't fire on:pull_request (recursion guard), so the
# path-filtered trigger above never reports on them and the PR would be
# blocked forever. The bump workflow dispatches this against each per-entry
# bump branch instead; the check run lands on the branch HEAD (= PR head)
# and satisfies the required check. The validate job runs unconditionally,
# so a dispatch always reports.
workflow_dispatch:
permissions:
contents: read

View File

@@ -355,9 +355,9 @@ def _list_untracked(cwd):
the holdouts."""
try:
repo = _git_toplevel(cwd) or cwd
# core.quotePath=false comes from GIT_CMD globally (see gitutil.py).
r = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false", "ls-files",
"--others", "--exclude-standard", "-z"],
[*GIT_CMD, "ls-files", "--others", "--exclude-standard", "-z"],
cwd=repo, capture_output=True, timeout=15,
)
if r.returncode != 0:

View File

@@ -26,6 +26,17 @@ GIT_CMD = [
"git",
"-c", "core.fsmonitor=false",
"-c", "core.hooksPath=/dev/null",
# core.quotePath=false: emit raw UTF-8 in path-emitting commands instead
# of C-quoting non-ASCII bytes (default `"\\303\\201vila/..."` vs
# `Ávila/...`). Downstream parsers — both ours (parse_diff_into_files,
# extract_file_paths_from_diff) and Python stdlib (os.path.isabs,
# os.path.join) — expect raw paths and silently drop / mishandle the
# quoted form. Adding the flag globally to GIT_CMD covers every
# subprocess.run site that uses the splat — diff feeders, rev-parse
# path queries (--show-toplevel, --git-dir, --git-common-dir),
# reflog %gs subjects, ls-files, status, etc. — without per-site
# flag duplication. See #2082, #2099.
"-c", "core.quotePath=false",
]
@@ -222,15 +233,12 @@ def _git_diff_range(repo_root, base, head="HEAD"):
them reviewed — otherwise unreviewed commits get permanently silenced.
"""
try:
# core.quotePath=false makes git emit raw UTF-8 in `diff --git a/... b/...`
# headers instead of C-quoting non-ASCII path bytes (`"a/\303\201vila/..."`
# vs `a/Ávila/...`). The downstream `re.match(r'^a/(.+?) b/(.+)$', ...)`
# in parse_diff_into_files / extract_file_paths_from_diff matches the
# raw form only — quoted headers slip past and the entire file is
# silently dropped from review. See #2082 (sibling of #2056 / #2075).
# GIT_CMD globally passes core.quotePath=false (see definition) so
# non-ASCII paths in `diff --git a/... b/...` headers come through as
# raw UTF-8, not C-quoted. Required by the downstream
# parse_diff_into_files / extract_file_paths_from_diff regex.
r = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false",
"diff", "-p", "--no-color", "--no-ext-diff", base, head],
[*GIT_CMD, "diff", "-p", "--no-color", "--no-ext-diff", base, head],
cwd=repo_root, capture_output=True, timeout=30,
)
if r.returncode != 0:
@@ -355,8 +363,9 @@ def _git_name_only(cwd, base, include_untracked=False):
# result.stdout=None, and propagate AttributeError out of the helper.
# Same fix shape as diffstate._list_untracked. See #2056.
def _run(env):
# core.quotePath=false comes from GIT_CMD globally (see definition).
result = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false", "diff", "--name-only", "-z", base],
[*GIT_CMD, "diff", "--name-only", "-z", base],
cwd=cwd, capture_output=True, timeout=30,
env=env,
)
@@ -393,9 +402,9 @@ def _git_status_porcelain(cwd):
# sibling helpers — a non-ASCII path in the worktree would otherwise
# crash the cp1252 reader thread on Windows. See #2056.
try:
# core.quotePath=false comes from GIT_CMD globally (see definition).
r = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false", "status",
"--porcelain=v1", "-uall", "-z"],
[*GIT_CMD, "status", "--porcelain=v1", "-uall", "-z"],
cwd=cwd, capture_output=True, timeout=30,
)
if r.returncode != 0:
@@ -471,11 +480,8 @@ def get_git_diff(cwd, baseline_sha, full_context=False, paths=None, untracked_pa
# change exists to fix.
return ""
# core.quotePath=false: emit raw UTF-8 in `diff --git a/... b/...` headers
# so non-ASCII paths aren't C-quoted past the downstream parse_diff_into_files
# regex. See #2082 (sibling of #2056 / #2075).
cmd = [*GIT_CMD, "-c", "core.quotePath=false",
"diff", "--no-color", "--no-ext-diff", baseline_sha] + (["--unified=99999"] if full_context else []) + pathspec
# core.quotePath=false comes from GIT_CMD globally (see definition).
cmd = [*GIT_CMD, "diff", "--no-color", "--no-ext-diff", baseline_sha] + (["--unified=99999"] if full_context else []) + pathspec
try:
with _temp_index(cwd, untracked_paths) as env:
# env is None when no index could be found (bare repo / not a

View File

@@ -479,10 +479,21 @@ def _call_claude(prompt, output_schema, thinking_budget=10000, max_tokens=16000,
"max_tokens": max_tokens,
"system": CLAUDE_CODE_SYSTEM_PROMPT,
"messages": [{"role": "user", "content": prompt}],
"output_format": {
"type": "json_schema",
"schema": output_schema
}
# API moved the structured-output schema from top-level `output_format`
# to `output_config.format` per
# https://platform.claude.com/docs/en/build-with-claude/structured-outputs.
# The old form "continues to work for a transition period" for some
# auth modes (API key + non-streaming), but is rejected with
# `invalid_request_error: output_format: This field is deprecated.
# Use 'output_config.format' instead.` for others (OAuth Bearer +
# newer CLI versions hit it consistently — reporter saw 462 errors
# in one day). See #2098.
"output_config": {
"format": {
"type": "json_schema",
"schema": output_schema,
},
},
}
if thinking_budget > 0:
# Models trained on adaptive thinking (4.6+) reject the budget_tokens
@@ -490,7 +501,10 @@ def _call_claude(prompt, output_schema, thinking_budget=10000, max_tokens=16000,
# models (4.5 and earlier, all 3.x) reject adaptive. Pick by model.
if _model_supports_adaptive_thinking(payload["model"]):
payload["thinking"] = {"type": "adaptive"}
payload["output_config"] = {"effort": "high"}
# Merge `effort` into the existing output_config dict (which
# now carries the `format` schema) rather than reassigning —
# otherwise the schema is silently overwritten. See #2098.
payload["output_config"]["effort"] = "high"
else:
payload["thinking"] = {
"type": "enabled",

View File

@@ -1197,18 +1197,18 @@ def handle_commit_review_posttooluse(input_data):
# core.quotePath=false: emit raw UTF-8 in `diff --git a/... b/...`
# headers so non-ASCII paths aren't C-quoted past the downstream
# parse_diff_into_files regex (sibling of #2056 / #2075). See #2082.
# core.quotePath=false comes from GIT_CMD globally (see gitutil.py).
if pre_amend_sha:
# Delta review: pre-amend → post-amend. `git diff` (not show)
# so the output is a pure unified diff with no commit header.
result = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false",
"diff", "--no-color", "--no-ext-diff", pre_amend_sha, sha, "--"],
[*GIT_CMD, "diff", "--no-color", "--no-ext-diff",
pre_amend_sha, sha, "--"],
cwd=repo_root, capture_output=True, timeout=15
)
else:
result = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false",
"show", "-p", "--no-color", "--no-ext-diff", sha, "--"],
[*GIT_CMD, "show", "-p", "--no-color", "--no-ext-diff", sha, "--"],
cwd=repo_root, capture_output=True, timeout=15
)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e: