From 150f567de845b1a7dd1e8f45b76f0f14ef3c0bac Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Sun, 12 Jul 2026 09:40:33 +0700 Subject: [PATCH] fix(history): ownership check crashes with UnicodeDecodeError on non-cp1252 git output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 5 subprocess.run() calls in ownership_engine.py used text=True without an explicit encoding, so Python decoded git blame/log/shortlog output with the platform's default locale codec — cp1252 on Windows — which crashes with UnicodeDecodeError on any byte outside cp1252's range (accented author names, non-ASCII commit text). Found via real-codebase validation: 'history --check ownership' crashed with "'NoneType' object has no attribute 'split'" against Coretax-Auto-Downloader's KDS backend, whose git history contains commits from authors with non-ASCII names/text. Fix: add encoding='utf-8', errors='replace' to all 5 calls — same pattern issue #179's _force_utf8_stdio() fix used for stdout/stderr, applied here to subprocess output instead. --- scripts/ownership_engine.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/ownership_engine.py b/scripts/ownership_engine.py index 1ccbf29b..6391966b 100755 --- a/scripts/ownership_engine.py +++ b/scripts/ownership_engine.py @@ -18,6 +18,15 @@ import json from typing import Dict, List, Any, Optional, Tuple from collections import defaultdict + +# All subprocess.run(..., text=True) calls below also pass +# encoding='utf-8', errors='replace'. Without an explicit encoding, +# subprocess decodes with the platform's default locale codec — cp1252 on +# Windows — which crashes with UnicodeDecodeError on git blame/log output +# containing non-cp1252 bytes (e.g. accented author names, non-ASCII commit +# text). Found via real-codebase validation: `history --check ownership` +# crashed with "'NoneType' object has no attribute 'split'" against a repo +# whose git history has UTF-8 characters outside cp1252's range. from datetime import datetime, timezone from utils import DEFAULT_IGNORE_DIRS, logger @@ -74,7 +83,7 @@ def _is_git_repo(workspace: str) -> bool: try: result = subprocess.run( ['git', 'rev-parse', '--is-inside-work-tree'], - cwd=workspace, capture_output=True, text=True, timeout=5 + cwd=workspace, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5 ) return result.returncode == 0 except (subprocess.SubprocessError, FileNotFoundError): @@ -86,7 +95,7 @@ def _run_git_blame(workspace: str, file_path: str) -> Optional[List[Dict]]: try: result = subprocess.run( ['git', 'blame', '--line-porcelain', file_path], - cwd=workspace, capture_output=True, text=True, timeout=30 + cwd=workspace, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=30 ) if result.returncode != 0: return None @@ -280,7 +289,7 @@ def _analyze_workspace_ownership(workspace: str) -> Dict[str, Any]: try: result = subprocess.run( ['git', 'rev-parse', '--is-shallow-repository'], - cwd=workspace, capture_output=True, text=True, timeout=5 + cwd=workspace, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5 ) if result.returncode == 0 and result.stdout.strip().lower() == 'true': is_shallow = True @@ -296,7 +305,7 @@ def _analyze_workspace_ownership(workspace: str) -> Dict[str, Any]: # Get commit count per author result = subprocess.run( ['git', 'shortlog', '-sn', '--all'], - cwd=workspace, capture_output=True, text=True, timeout=15 + cwd=workspace, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=15 ) if result.returncode == 0: for line in result.stdout.strip().split('\n'): @@ -343,7 +352,7 @@ def _analyze_workspace_ownership(workspace: str) -> Dict[str, Any]: try: result = subprocess.run( ['git', 'log', '-1', '--format=%ct', '--', rel_path], - cwd=workspace, capture_output=True, text=True, timeout=5 + cwd=workspace, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5 ) if result.returncode == 0 and result.stdout.strip(): ts = int(result.stdout.strip())