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 <cursoragent@cursor.com>
This commit is contained in:
mihailkudravcev
2026-07-24 13:23:32 +03:00
parent dc8be1bd1a
commit 98cbb88461
6 changed files with 66 additions and 13 deletions
+6
View File
@@ -2,6 +2,12 @@
All notable changes to **export_1c_help** are documented in this file. 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 ## [0.3.6] - 2026-07-24
### Fixed ### Fixed
+1 -1
View File
@@ -1 +1 @@
0.3.6 0.3.7
+2 -1
View File
@@ -9,7 +9,8 @@ _VERSION_FILE = Path(__file__).resolve().parent.parent / "VERSION"
def _read_version() -> str: def _read_version() -> str:
try: 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: except OSError:
return "0.0.0" return "0.0.0"
+13 -6
View File
@@ -76,6 +76,11 @@ def read_config_name(src_root: Path) -> str | None:
return read_config_identity(src_root).name 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: def read_config_identity(src_root: Path) -> ConfigIdentity:
"""Version, Name, Synonym (ru), Vendor from dump.""" """Version, Name, Synonym (ru), Vendor from dump."""
src_root = Path(src_root) src_root = Path(src_root)
@@ -84,7 +89,9 @@ def read_config_identity(src_root: Path) -> ConfigIdentity:
version = "unknown" version = "unknown"
version_file = root / "VERSION" version_file = root / "VERSION"
if version_file.is_file(): 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: if text:
version = text.splitlines()[0].strip() version = text.splitlines()[0].strip()
@@ -103,22 +110,22 @@ def read_config_identity(src_root: Path) -> ConfigIdentity:
if version == "unknown": if version == "unknown":
m = _VERSION_TAG.search(props) m = _VERSION_TAG.search(props)
if m: if m:
version = m.group(1).strip() version = _sanitize_text(m.group(1)) or "unknown"
m = _NAME_TAG.search(props) m = _NAME_TAG.search(props)
if m: if m:
name = m.group(1).strip() or None name = _sanitize_text(m.group(1)) or None
m = _SYN_RE.search(props) m = _SYN_RE.search(props)
if m: 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) m = _VENDOR_TAG.search(props)
if m: if m:
vendor = _xml_unescape(m.group(1).strip()) or None vendor = _sanitize_text(_xml_unescape(m.group(1))) or None
return ConfigIdentity( return ConfigIdentity(
version=version or "unknown", version=_sanitize_text(version) or "unknown",
name=name, name=name,
synonym=synonym, synonym=synonym,
vendor=vendor, vendor=vendor,
+41 -4
View File
@@ -3,8 +3,10 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import shutil import shutil
import subprocess import subprocess
import tempfile
from pathlib import Path from pathlib import Path
from export1c_help.fscompat import copy_file, ensure_dir, remove_file from export1c_help.fscompat import copy_file, ensure_dir, remove_file
@@ -18,21 +20,56 @@ class WikiPushError(RuntimeError):
pass 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: 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( proc = subprocess.run(
cmd, clean_cmd,
cwd=cwd, cwd=clean_cwd,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8",
errors="replace",
check=False, check=False,
) )
if proc.returncode != 0: if proc.returncode != 0:
raise WikiPushError( 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 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( def prepare_wiki_clone(
wiki_url: str, wiki_url: str,
work_dir: Path, work_dir: Path,
@@ -149,5 +186,5 @@ def push_wiki(
if not status.strip(): if not status.strip():
return return
_run(["git", "commit", "-m", message], cwd=work_dir) _git_commit(work_dir, message)
_run(["git", "push", "origin", f"HEAD:{branch}"], cwd=work_dir) _run(["git", "push", "origin", f"HEAD:{branch}"], cwd=work_dir)
+3 -1
View File
@@ -297,7 +297,9 @@ def cmd_push(args: argparse.Namespace) -> int:
message = args.message or ( message = args.message or (
f"export_1c_help v{__version__}: sync help cfg {stats.config_version} " 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: try:
if not (work / ".git").exists(): if not (work / ".git").exists():