🚧 Update commit message format
This commit is contained in:
@@ -171,7 +171,12 @@ class ApplicationController:
|
||||
"Author resolved: %s -> %s", record.path, result.identifier
|
||||
)
|
||||
|
||||
message = build_commit_message(kept, run, app_version=_APP_VERSION)
|
||||
message = build_commit_message(
|
||||
kept,
|
||||
run,
|
||||
app_version=_APP_VERSION,
|
||||
file_separator=self.config.commit.file_separator,
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
log.info("Dry-run: repository left unmodified")
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
Human-readable sections first, Git trailer block last so that
|
||||
`git interpret-trailers` can extract the structured metadata.
|
||||
|
||||
Each changed file occupies exactly ONE line in `git diff --name-status`
|
||||
style: a change letter followed by path, author and last-modified time,
|
||||
joined by a configurable separator (commit.file_separator, default ';').
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
|
||||
from git_auto_tracker.changes.models import ChangeSet, TrackerRun
|
||||
from git_auto_tracker.changes.models import ChangeRecord, ChangeSet, ChangeType, TrackerRun
|
||||
|
||||
_SUMMARY_LABELS = (
|
||||
("Added:", "added"),
|
||||
@@ -17,6 +21,13 @@ _SUMMARY_LABELS = (
|
||||
("Renamed:", "renamed"),
|
||||
)
|
||||
|
||||
_GIT_LETTER = {
|
||||
ChangeType.ADDED: "A",
|
||||
ChangeType.MODIFIED: "M",
|
||||
ChangeType.DELETED: "D",
|
||||
ChangeType.RENAMED: "R",
|
||||
}
|
||||
|
||||
|
||||
def _format_ts(moment: dt.datetime | None) -> str:
|
||||
"""Render an aware datetime in local time with an explicit offset.
|
||||
@@ -30,20 +41,28 @@ def _format_ts(moment: dt.datetime | None) -> str:
|
||||
return moment.astimezone().isoformat(sep=" ", timespec="seconds")
|
||||
|
||||
|
||||
def _author_lines(record) -> list[str]: # noqa: ANN001 - ChangeRecord, avoid cycle
|
||||
lines: list[str] = []
|
||||
author = record.author
|
||||
identifier = author.identifier if author is not None else "UNKNOWN"
|
||||
lines.append(f" Last modified by: {identifier}")
|
||||
# Filesystem last-modified time (Section 12: file_timestamp); rendered
|
||||
# in local time like the log timestamps. Resolver-provided timestamps
|
||||
# (author.timestamp) stay authoritative and are shown as well.
|
||||
file_ts = _format_ts(record.file_timestamp)
|
||||
if file_ts:
|
||||
lines.append(f" Modified at: {file_ts}")
|
||||
if author is not None and author.timestamp is not None:
|
||||
lines.append(f" Changed at: {_format_ts(author.timestamp)}")
|
||||
return lines
|
||||
def _file_line(record: ChangeRecord, separator: str) -> str:
|
||||
"""One line per file, git --name-status style (Section 15).
|
||||
|
||||
Grammar:
|
||||
A <sep> <path> <sep> <author> <sep> <mtime> added
|
||||
M <sep> <path> <sep> <author> <sep> <mtime> modified
|
||||
R <sep> <old-path> -> <new-path> renamed (bare)
|
||||
D <sep> <path> deleted (bare)
|
||||
|
||||
The filesystem last-modified time (Section 12: file_timestamp) is used
|
||||
for the time column, rendered in local time like the log timestamps.
|
||||
Deleted and renamed entries stay bare: the file is gone (FR-013), and a
|
||||
pure rename preserves the original mtime so a timestamp would mislead.
|
||||
"""
|
||||
letter = _GIT_LETTER[record.change_type]
|
||||
if record.change_type is ChangeType.RENAMED:
|
||||
return f"{letter} {separator} {record.previous_path or record.path} -> {record.path}"
|
||||
if record.change_type is ChangeType.DELETED:
|
||||
return f"{letter} {separator} {record.path}"
|
||||
author = record.author.identifier if record.author is not None else "UNKNOWN"
|
||||
parts = [letter, record.path, author, _format_ts(record.file_timestamp)]
|
||||
return f" {separator} ".join(parts)
|
||||
|
||||
|
||||
def build_commit_message(
|
||||
@@ -51,6 +70,7 @@ def build_commit_message(
|
||||
run: TrackerRun,
|
||||
collected_at: dt.datetime | None = None,
|
||||
app_version: str = "1.0.0",
|
||||
file_separator: str = ";",
|
||||
) -> str:
|
||||
"""Build the complete commit message per Sections 15/16/36."""
|
||||
counts = change_set.summary_counts()
|
||||
@@ -64,23 +84,10 @@ def build_commit_message(
|
||||
lines.append(f" {label} {counts[key]}")
|
||||
lines.append("")
|
||||
|
||||
for heading, records in (
|
||||
("Added:", change_set.added),
|
||||
("Modified:", change_set.modified),
|
||||
("Renamed:", change_set.renamed),
|
||||
("Deleted:", change_set.deleted),
|
||||
):
|
||||
if not records:
|
||||
continue
|
||||
lines.append(heading)
|
||||
for record in records:
|
||||
if record.change_type.value == "RENAMED" and record.previous_path:
|
||||
lines.append(f" {record.previous_path} -> {record.path}")
|
||||
else:
|
||||
lines.append(f" {record.path}")
|
||||
if record.needs_author:
|
||||
lines.extend(_author_lines(record))
|
||||
lines.append("")
|
||||
lines.append("Changes:")
|
||||
for record in change_set.all_records():
|
||||
lines.append(f" {_file_line(record, file_separator)}")
|
||||
lines.append("")
|
||||
|
||||
# Structured trailers (Section 36)
|
||||
lines.append(f"Tracker-Version: {app_version}")
|
||||
|
||||
@@ -90,6 +90,24 @@ class LoggingConfig:
|
||||
backup_count: int = 10
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommitConfig:
|
||||
"""Commit-message rendering options (Sections 15 and 16)."""
|
||||
|
||||
#: Separator between the parts of a one-line file entry. Must not be
|
||||
#: empty; must not occur inside paths, author identifiers or timestamps.
|
||||
file_separator: str = ";"
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> CommitConfig:
|
||||
separator = data.get("file_separator", ";")
|
||||
if not isinstance(separator, str) or not separator.strip():
|
||||
raise ConfigurationError("commit.file_separator must be a non-empty string")
|
||||
if separator in ("\n", "\r"):
|
||||
raise ConfigurationError("commit.file_separator must not be a line break")
|
||||
return cls(file_separator=separator)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
repository_path: Path
|
||||
@@ -101,6 +119,7 @@ class AppConfig:
|
||||
author_resolution: AuthorResolutionConfig
|
||||
logging: LoggingConfig
|
||||
timezone: str | None = None
|
||||
commit: CommitConfig = field(default_factory=CommitConfig)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any], config_dir: Path) -> AppConfig:
|
||||
@@ -152,6 +171,7 @@ class AppConfig:
|
||||
max_size_mb=int(logging_cfg.get("max_size_mb", 20)),
|
||||
backup_count=int(logging_cfg.get("backup_count", 10)),
|
||||
),
|
||||
commit=CommitConfig.from_dict(data.get("commit", {})),
|
||||
timezone=app_cfg.get("timezone"),
|
||||
)
|
||||
except ConfigurationError:
|
||||
|
||||
Reference in New Issue
Block a user