commit e31382fabbeb9b957963f4e89f07f9ec41693607
parent 8eb84fabd7a714c26f97dfa87ff3f1543e609b03
Author: Amit Dutta <mail@amit.is-a.dev>
Date: Thu, 20 Aug 2026 13:19:07 +0530
Merge pull request #54 from notamitgamer/feat/detailed-changelog
feat: detailed changelog with per-commit file diffs
Diffstat:
2 files changed, 148 insertions(+), 29 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
@@ -91,7 +91,8 @@ jobs:
BUILD_REF: ${{ github.ref_name }}
BUILD_TIMESTAMP: ${{ github.event.head_commit.timestamp }}
run: |
- git log --pretty=format:"%H|%s|%an|%aI" -20 > git_log.txt
+ git log -10 --pretty=format:"COMMIT|%H|%s|%an|%aI" --numstat > git_log_numstat.txt
+ git log -10 --pretty=format:"COMMIT|%H" --name-status > git_log_namestatus.txt
python changelog.py
- name: Commit generated changelog to repo
diff --git a/changelog.py b/changelog.py
@@ -9,41 +9,160 @@ timestamp = os.environ.get('BUILD_TIMESTAMP', '')
try:
dt = datetime.fromisoformat(timestamp).astimezone(timezone.utc)
build_time = dt.strftime('%B %d, %Y at %H:%M UTC')
-except:
+except Exception:
build_time = timestamp
-# Parse git log
+STATUS_LABELS = {
+ 'A': 'Added',
+ 'M': 'Modified',
+ 'D': 'Removed',
+ 'T': 'Type changed',
+ 'U': 'Unmerged',
+}
+
+
+def status_label(code):
+ letter = code[0]
+ if letter == 'R':
+ return f'Renamed ({code[1:]}%)' if len(code) > 1 else 'Renamed'
+ if letter == 'C':
+ return f'Copied ({code[1:]}%)' if len(code) > 1 else 'Copied'
+ return STATUS_LABELS.get(letter, letter)
+
+
+# ---- Pass 1: commit metadata + numstat (line/diff counts) ----
commits = []
+commit_index = {} # full_sha -> index in commits
+
try:
- with open('git_log.txt', 'r', encoding='utf-8') as f:
- for line in f:
- parts = line.strip().split('|')
- if len(parts) == 4:
- c_sha, c_msg, c_author, c_time = parts
- try:
- c_dt = datetime.fromisoformat(c_time).astimezone(timezone.utc)
- c_time_fmt = c_dt.strftime('%b %d, %Y %H:%M UTC')
- except:
- c_time_fmt = c_time
- commits.append({
- 'sha': c_sha[:7],
- 'full_sha': c_sha,
- 'msg': c_msg,
- 'author': c_author,
- 'time': c_time_fmt,
- })
+ with open('git_log_numstat.txt', 'r', encoding='utf-8') as f:
+ current = None
+ for raw_line in f:
+ line = raw_line.rstrip('\n')
+ if line.startswith('COMMIT|'):
+ parts = line.split('|')
+ if len(parts) == 5:
+ _, c_sha, c_msg, c_author, c_time = parts
+ try:
+ c_dt = datetime.fromisoformat(c_time).astimezone(timezone.utc)
+ c_time_fmt = c_dt.strftime('%b %d, %Y %H:%M UTC')
+ except Exception:
+ c_time_fmt = c_time
+ current = {
+ 'sha': c_sha[:7],
+ 'full_sha': c_sha,
+ 'msg': c_msg,
+ 'author': c_author,
+ 'time': c_time_fmt,
+ 'files': {}, # path -> {'add': int, 'del': int, 'status': str}
+ 'order': [], # preserve file order
+ }
+ commit_index[c_sha] = len(commits)
+ commits.append(current)
+ continue
+ if not line.strip() or current is None:
+ continue
+ # numstat line: "<added>\t<deleted>\t<path>" (binary files use '-')
+ cols = line.split('\t')
+ if len(cols) == 3:
+ add, dele, path = cols
+ add_n = int(add) if add.isdigit() else 0
+ del_n = int(dele) if dele.isdigit() else 0
+ current['files'][path] = {'add': add_n, 'del': del_n, 'status': 'M'}
+ current['order'].append(path)
+except FileNotFoundError:
+ pass
+
+# ---- Pass 2: name-status (A/M/D/R/C) merged into the same commits ----
+try:
+ with open('git_log_namestatus.txt', 'r', encoding='utf-8') as f:
+ current = None
+ for raw_line in f:
+ line = raw_line.rstrip('\n')
+ if line.startswith('COMMIT|'):
+ parts = line.split('|')
+ if len(parts) == 2:
+ c_sha = parts[1]
+ idx = commit_index.get(c_sha)
+ current = commits[idx] if idx is not None else None
+ continue
+ if not line.strip() or current is None:
+ continue
+ cols = line.split('\t')
+ if len(cols) == 2:
+ code, path = cols
+ if path in current['files']:
+ current['files'][path]['status'] = code
+ else:
+ current['files'][path] = {'add': 0, 'del': 0, 'status': code}
+ current['order'].append(path)
+ elif len(cols) == 3:
+ code, old_path, new_path = cols
+ target = new_path
+ stats = current['files'].pop(old_path, {'add': 0, 'del': 0, 'status': code})
+ stats['status'] = code
+ current['files'][target] = stats
+ if old_path in current['order']:
+ current['order'][current['order'].index(old_path)] = target
+ else:
+ current['order'].append(target)
except FileNotFoundError:
pass
ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline; margin-bottom:-2px; margin-right:6px;" class="lucide lucide-history"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg>'
-# Build commit history markdown
-commit_lines = []
-for c in commits:
- commit_url = f"https://github.com/notamitgamer/bsc/commit/{c['full_sha']}"
- commit_lines.append(f"- [`{c['sha']}`]({commit_url}) {c['msg']} — {c['author']}, {c['time']}")
-commit_section = '\n'.join(commit_lines) if commit_lines else '- No commit history available.'
+def build_commit_block(c):
+ commit_url = f"https://github.com/notamitgamer/bsc/commit/{c['full_sha']}"
+ total_add = sum(v['add'] for v in c['files'].values())
+ total_del = sum(v['del'] for v in c['files'].values())
+ file_count = len(c['files'])
+
+ header = (
+ f"### [`{c['sha']}`]({commit_url}) {c['msg']}\n\n"
+ f"**{c['author']}** committed on {c['time']}"
+ )
+
+ if file_count:
+ stat_bits = []
+ if total_add or total_del:
+ stat_bits.append(f"+{total_add} / -{total_del} lines")
+ stat_bits.append(f"{file_count} file{'s' if file_count != 1 else ''} changed")
+ header += f" · {' · '.join(stat_bits)}"
+
+ if not file_count:
+ return header + "\n"
+
+ file_lines = []
+ for path in c['order']:
+ info = c['files'][path]
+ label = status_label(info['status'])
+ diff_bits = []
+ if info['add']:
+ diff_bits.append(f"+{info['add']}")
+ if info['del']:
+ diff_bits.append(f"-{info['del']}")
+ diff_str = ' '.join(diff_bits) if diff_bits else 'binary/no diff'
+ file_lines.append(f"| `{path}` | {label} | {diff_str} |")
+
+ files_table = (
+ "| File | Change | Lines |\n"
+ "| --- | --- | --- |\n"
+ + '\n'.join(file_lines)
+ )
+
+ details = (
+ "<details>\n"
+ f"<summary>Show {file_count} changed file{'s' if file_count != 1 else ''}</summary>\n\n"
+ f"{files_table}\n"
+ "</details>\n"
+ )
+
+ return f"{header}\n\n{details}"
+
+
+commit_blocks = [build_commit_block(c) for c in commits]
+commit_section = '\n\n---\n\n'.join(commit_blocks) if commit_blocks else 'No commit history available.'
# VitePress docs/changelog.md
docs_content = f"""---
@@ -55,7 +174,7 @@ description: 'Current build information and recent commit history.'
::: tip Important
Compare the Build ID (listed below) against the one in the [GitHub Changelog](https://github.com/notamitgamer/bsc/blob/main/CHANGELOG.md#latest-build) to verify that your browser is displaying the latest version.
-:::
+:::
- **Build ID** — <span style="word-break: break-all;">`{sha}`</span>
- **Triggered by** — [@{actor}](https://github.com/{actor})
@@ -95,4 +214,4 @@ root_content = f"""# Changelog
with open('CHANGELOG.md', 'w', encoding='utf-8') as f:
f.write(root_content)
-print(f"Generated changelog with {len(commits)} commits.")-
\ No newline at end of file
+print(f"Generated changelog with {len(commits)} commits.")