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:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user