From 98cbb8846175c2b7c26fe6d0232be7efba497a8d Mon Sep 17 00:00:00 2001 From: mihailkudravcev Date: Fri, 24 Jul 2026 13:23:32 +0300 Subject: [PATCH] Fix Windows git commit ValueError: embedded null character. Use git commit -F and strip NULs from argv/version strings (0.3.7). Co-authored-by: Cursor --- CHANGELOG.md | 6 +++++ VERSION | 2 +- export1c_help/__version__.py | 3 ++- export1c_help/config_version.py | 19 +++++++++----- export1c_help/wiki.py | 45 ++++++++++++++++++++++++++++++--- export_1c_help.py | 4 ++- 6 files changed, 66 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea68ca8..0235635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to **export_1c_help** are documented in this file. +## [0.3.7] - 2026-07-24 + +### Fixed + +- Windows: `git commit` no longer fails with `ValueError: embedded null character` — commit message is written via `-F` and NULs are stripped from argv/version strings + ## [0.3.6] - 2026-07-24 ### Fixed diff --git a/VERSION b/VERSION index 449d7e7..0f82685 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.6 +0.3.7 diff --git a/export1c_help/__version__.py b/export1c_help/__version__.py index 363425b..76b7d33 100644 --- a/export1c_help/__version__.py +++ b/export1c_help/__version__.py @@ -9,7 +9,8 @@ _VERSION_FILE = Path(__file__).resolve().parent.parent / "VERSION" def _read_version() -> str: try: - return _VERSION_FILE.read_text(encoding="utf-8").strip() + text = _VERSION_FILE.read_text(encoding="utf-8-sig", errors="ignore") + return text.replace("\0", "").strip() or "0.0.0" except OSError: return "0.0.0" diff --git a/export1c_help/config_version.py b/export1c_help/config_version.py index bc9cc91..8d8716f 100644 --- a/export1c_help/config_version.py +++ b/export1c_help/config_version.py @@ -76,6 +76,11 @@ def read_config_name(src_root: Path) -> str | None: return read_config_identity(src_root).name +def _sanitize_text(s: str) -> str: + """Drop NULs (UTF-16 mis-reads / dirty dumps) that break Windows APIs.""" + return s.replace("\0", "").strip() + + def read_config_identity(src_root: Path) -> ConfigIdentity: """Version, Name, Synonym (ru), Vendor from dump.""" src_root = Path(src_root) @@ -84,7 +89,9 @@ def read_config_identity(src_root: Path) -> ConfigIdentity: version = "unknown" version_file = root / "VERSION" if version_file.is_file(): - text = version_file.read_text(encoding="utf-8-sig", errors="ignore").strip() + text = _sanitize_text( + version_file.read_text(encoding="utf-8-sig", errors="ignore") + ) if text: version = text.splitlines()[0].strip() @@ -103,22 +110,22 @@ def read_config_identity(src_root: Path) -> ConfigIdentity: if version == "unknown": m = _VERSION_TAG.search(props) if m: - version = m.group(1).strip() + version = _sanitize_text(m.group(1)) or "unknown" m = _NAME_TAG.search(props) if m: - name = m.group(1).strip() or None + name = _sanitize_text(m.group(1)) or None m = _SYN_RE.search(props) if m: - synonym = _xml_unescape(m.group(1).strip()) or None + synonym = _sanitize_text(_xml_unescape(m.group(1))) or None m = _VENDOR_TAG.search(props) if m: - vendor = _xml_unescape(m.group(1).strip()) or None + vendor = _sanitize_text(_xml_unescape(m.group(1))) or None return ConfigIdentity( - version=version or "unknown", + version=_sanitize_text(version) or "unknown", name=name, synonym=synonym, vendor=vendor, diff --git a/export1c_help/wiki.py b/export1c_help/wiki.py index 6e36640..f4ed440 100644 --- a/export1c_help/wiki.py +++ b/export1c_help/wiki.py @@ -3,8 +3,10 @@ from __future__ import annotations import json +import os import shutil import subprocess +import tempfile from pathlib import Path from export1c_help.fscompat import copy_file, ensure_dir, remove_file @@ -18,21 +20,56 @@ class WikiPushError(RuntimeError): pass +def _clean_arg(value: str | Path) -> str: + """Windows CreateProcess rejects argv/cwd strings that contain NUL.""" + s = os.fspath(value) + if "\0" in s: + s = s.replace("\0", "") + return s + + def _run(cmd: list[str], *, cwd: Path) -> str: + clean_cmd = [_clean_arg(a) for a in cmd] + clean_cwd = _clean_arg(cwd.resolve()) proc = subprocess.run( - cmd, - cwd=cwd, + clean_cmd, + cwd=clean_cwd, capture_output=True, text=True, + encoding="utf-8", + errors="replace", check=False, ) if proc.returncode != 0: raise WikiPushError( - f"$ {' '.join(cmd)}\n{proc.stdout}\n{proc.stderr}".strip() + f"$ {' '.join(clean_cmd)}\n{proc.stdout}\n{proc.stderr}".strip() ) return proc.stdout +def _git_commit(work_dir: Path, message: str) -> None: + """Commit via ``-F`` so Windows never gets NUL/encoding issues in ``-m``.""" + text = _clean_arg(message).strip() or "export_1c_help: sync help" + if not text.endswith("\n"): + text += "\n" + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + newline="\n", + suffix=".txt", + delete=False, + ) as fh: + fh.write(text) + msg_path = fh.name + try: + _run(["git", "commit", "-F", msg_path], cwd=work_dir) + finally: + try: + Path(msg_path).unlink() + except OSError: + pass + + def prepare_wiki_clone( wiki_url: str, work_dir: Path, @@ -149,5 +186,5 @@ def push_wiki( if not status.strip(): return - _run(["git", "commit", "-m", message], cwd=work_dir) + _git_commit(work_dir, message) _run(["git", "push", "origin", f"HEAD:{branch}"], cwd=work_dir) diff --git a/export_1c_help.py b/export_1c_help.py index a506bcd..c4e178c 100644 --- a/export_1c_help.py +++ b/export_1c_help.py @@ -297,7 +297,9 @@ def cmd_push(args: argparse.Namespace) -> int: message = args.message or ( f"export_1c_help v{__version__}: sync help cfg {stats.config_version} " - f"({stats.pages_written} pages, +{stats.created_count}/~{stats.edited_count}/-{stats.deleted_count})" + f"({stats.pages_written} pages, " + f"+{stats.created_count} ~{stats.edited_count} " + f"={stats.unchanged_count} -{stats.deleted_count})" ) try: if not (work / ".git").exists():