90 since_ref: str |
None =
None,
91) -> list[ChangelogEntry]:
92 """Read git log, parse each conventional commit, return entries.
94 Non-conventional commits are silently skipped. If *since_ref* is
95 ``None``, the latest tag is used; if no tags exist, the entire
98 func =
"collect_changelog_entries"
99 _dbg(
"GIT", func, f
"repo_root={repo_root} since_ref={since_ref!r}", 2)
101 from oct.core.git
import list_tags, get_commit_log
104 effective_ref = since_ref
105 if effective_ref
is None:
106 tags = list_tags(repo_root)
108 effective_ref = tags[0]
109 _dbg(
"GIT", func, f
"using latest tag: {effective_ref}", 3)
114 raw_lines = get_commit_log(
116 since_ref=effective_ref,
117 format_str=f
"%h{sep}%B%x00",
122 raw_text =
"\n".join(raw_lines)
123 raw_commits = [c.strip()
for c
in raw_text.split(
"\x00")
if c.strip()]
125 entries: list[ChangelogEntry] = []
126 for raw
in raw_commits:
130 sha_part, message = raw.split(sep, 1)
131 sha = sha_part.strip()
132 message = message.strip()
134 parsed = parse_commit_message(message)
136 _dbg(
"GIT", func, f
"skipping non-conventional: {sha}", 4)
139 section = _TYPE_TO_SECTION.get(parsed.type)
141 _dbg(
"GIT", func, f
"skipping unmapped type: {parsed.type}", 4)
147 subject=parsed.subject,
148 breaking=parsed.breaking,
152 _dbg(
"GIT", func, f
"collected {len(entries)} entries", 2)
162 entries: list[ChangelogEntry],
163 version: str |
None =
None,
165 """Render entries in Keep a Changelog format.
167 Groups entries by section in the canonical order. If *version* is
168 provided, the header uses ``## [version] - YYYY-MM-DD``; otherwise
171 lines: list[str] = []
175 lines.append(f
"## [{version}] - {date.today().isoformat()}")
177 lines.append(
"## [Unreleased]")
181 grouped: dict[str, list[ChangelogEntry]] = {}
182 for entry
in entries:
183 grouped.setdefault(entry.section, []).append(entry)
186 lines.append(
"No notable changes.")
188 return "\n".join(lines)
190 for section
in _SECTION_ORDER:
191 section_entries = grouped.get(section)
192 if not section_entries:
194 lines.append(f
"### {section}")
196 for entry
in section_entries:
197 prefix =
"**BREAKING:** " if entry.breaking
else ""
198 scope = f
"**{entry.scope}:** " if entry.scope
else ""
199 lines.append(f
"- {prefix}{scope}{entry.subject} ({entry.sha})")
202 return "\n".join(lines)
206 entries: list[ChangelogEntry],
207 version: str |
None =
None,
209 """Return structured dict for ``--json`` output."""
210 grouped: dict[str, list[dict]] = {}
211 for entry
in entries:
212 grouped.setdefault(entry.section, []).append({
213 "scope": entry.scope,
214 "subject": entry.subject,
215 "breaking": entry.breaking,
220 "version": version
or "Unreleased",
221 "date": date.today().isoformat(),
223 section: grouped.get(section, [])
224 for section
in _SECTION_ORDER
225 if grouped.get(section)
227 "total_entries": len(entries),