add blog entry

This commit is contained in:
2026-08-13 18:53:18 -07:00
parent 4729b974cc
commit c0cbdac8af
9577 changed files with 2301587 additions and 3557 deletions
+83 -1
View File
@@ -7,6 +7,7 @@ from dataclasses import dataclass, replace
import os
from pathlib import Path
import re
import shutil
import subprocess
from urllib.parse import urlsplit, urlunsplit
@@ -66,6 +67,13 @@ class RepositoryState:
def can_push(self) -> bool:
return self.sync_state == LOCAL_AHEAD
@property
def can_publish(self) -> bool:
return (
self.sync_state in {SYNCED, LOCAL_AHEAD}
and (self.dirty_count > 0 or bool(self.ahead))
)
class GitRepository:
"""Narrow wrapper around the installed Git CLI for one working tree."""
@@ -156,6 +164,63 @@ class GitRepository:
raise GitRepositoryError(f"Push failed: {self._safe_detail(result)}")
return self.refresh()
def publishing_changes(self) -> str:
"""Return every path that a publish operation would stage."""
result = self._git(
"status",
"--short",
"--untracked-files=all",
check=True,
)
return result.stdout.rstrip()
def publish(
self,
expected: RepositoryState,
commit_message: str | None,
expected_changes: str,
) -> RepositoryState:
"""Commit reviewed working-tree changes, then perform the guarded push."""
current = self.refresh()
self._verify_action_state(expected, current)
current_changes = self.publishing_changes()
if current_changes != expected_changes:
raise GitRepositoryError(
"The working-tree changes changed after confirmation. Review the "
"refreshed list before publishing."
)
if current.sync_state == DIVERGED:
raise GitRepositoryError(self._not_actionable_message(current, "publish"))
if current.sync_state == REMOTE_AHEAD:
raise GitRepositoryError(
"The remote has newer commits. Update the checkout before publishing."
)
if current.problem:
raise GitRepositoryError(current.problem)
if current.dirty_count:
message = " ".join((commit_message or "").splitlines()).strip()
if not message:
raise GitRepositoryError(
"A non-empty commit message is required to publish working-tree changes."
)
add_result = self._git("add", "--all", check=False)
if add_result.returncode != 0:
raise GitRepositoryError(
f"Could not stage the reviewed changes: {self._safe_detail(add_result)}"
)
commit_result = self._git("commit", "-m", message, check=False, timeout=60)
if commit_result.returncode != 0:
raise GitRepositoryError(
"Could not create the publishing commit. The changes remain safe "
f"locally and may be staged: {self._safe_detail(commit_result)}"
)
current = self.refresh()
if current.sync_state != LOCAL_AHEAD:
raise GitRepositoryError(self._not_actionable_message(current, "publish"))
return self.push(current)
def _read_state(self, *, compare_remote: bool) -> RepositoryState:
self._verify_root()
branch_result = self._git(
@@ -384,7 +449,11 @@ def _run_git(
check: bool,
timeout: int = 15,
) -> subprocess.CompletedProcess[str]:
command = ["git", *args]
executable, bundled = _git_executable(cwd)
command = [str(executable)]
if bundled:
command.extend(("-c", "credential.helper=manager"))
command.extend(args)
options: dict[str, object] = {
"cwd": cwd,
"capture_output": True,
@@ -410,6 +479,19 @@ def _run_git(
return result
def _git_executable(start: Path) -> tuple[Path | str, bool]:
"""Use the user's Git when available, with bundled PortableGit as fallback."""
system_git = shutil.which("git")
if system_git:
return system_git, False
bundled = start.resolve() / "support" / "PortableGit" / "cmd" / "git.exe"
if bundled.is_file():
return bundled, True
raise GitRepositoryError(
"Git is unavailable. Place PortableGit under support\\PortableGit or install Git."
)
def _error_detail(result: subprocess.CompletedProcess[str]) -> str:
return (result.stderr.strip() or result.stdout.strip() or "unknown Git error").replace(
"\n", " "