Skip to content

Commit e43fb4a

Browse files
authored
Create generate_changelog.py
1 parent 866ad86 commit e43fb4a

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

generate_changelog.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Auto-Changelog Generator
4+
------------------------
5+
Scans the last N commits from the git log and generates a
6+
Markdown-formatted summary of changes, categorized by type.
7+
"""
8+
9+
import subprocess
10+
import datetime
11+
import re
12+
import sys
13+
14+
# --- Configuration ---
15+
DEFAULT_COMMITS_TO_SCAN = 10 # Default number of commits to look back
16+
OUTPUT_FILE = "DRAFT_CHANGELOG.md"
17+
18+
# Keywords to categorize commits
19+
CATEGORIES = {
20+
"✨ New Content": ["create", "add", "new", "init"],
21+
"🐛 Fixes": ["fix", "repair", "resolve", "broken", "bug", "correct"],
22+
"♻️ Updates & Refactors": ["update", "refactor", "move", "rename", "clean", "structure"],
23+
"📚 Documentation": ["readme", "doc", "comment", "guide"],
24+
"🗑️ Removals": ["remove", "delete", "prune"]
25+
}
26+
27+
def run_git_command(command):
28+
"""Runs a git command and returns the output string."""
29+
try:
30+
result = subprocess.check_output(command, shell=True, text=True, stderr=subprocess.STDOUT)
31+
return result.strip()
32+
except subprocess.CalledProcessError as e:
33+
print(f"Error running git command: {e.output}")
34+
return ""
35+
36+
def parse_commits(num_commits):
37+
"""Gets commit messages and categorizes them."""
38+
# Get hash, subject, and date
39+
log_output = run_git_command(f'git log -n {num_commits} --pretty=format:"%h|%s|%ad" --date=short')
40+
41+
if not log_output:
42+
return {}, 0
43+
44+
categorized = {k: [] for k in CATEGORIES.keys()}
45+
categorized["⚡ Other Changes"] = [] # Fallback category
46+
47+
total_commits = 0
48+
49+
for line in log_output.split('\n'):
50+
if not line: continue
51+
total_commits += 1
52+
parts = line.split('|')
53+
if len(parts) < 3: continue
54+
55+
sha, msg, date = parts[0], parts[1], parts[2]
56+
entry = f"- {msg} (`{sha}`)"
57+
58+
# Categorize
59+
matched = False
60+
msg_lower = msg.lower()
61+
62+
for cat, keywords in CATEGORIES.items():
63+
if any(keyword in msg_lower for keyword in keywords):
64+
categorized[cat].append(entry)
65+
matched = True
66+
break
67+
68+
if not matched:
69+
categorized["⚡ Other Changes"].append(entry)
70+
71+
return categorized, total_commits
72+
73+
def get_files_changed(num_commits):
74+
"""Gets a list of files changed in the range."""
75+
# git diff --name-status HEAD~N..HEAD
76+
output = run_git_command(f"git diff --name-status HEAD~{num_commits}..HEAD")
77+
78+
stats = {
79+
"Modified": 0,
80+
"Added": 0,
81+
"Deleted": 0,
82+
"Renamed": 0
83+
}
84+
file_list = []
85+
86+
for line in output.split('\n'):
87+
if not line: continue
88+
parts = line.split('\t')
89+
status_code = parts[0][0] # M, A, D, R
90+
filename = parts[-1]
91+
92+
file_list.append(filename)
93+
94+
if status_code == 'M': stats["Modified"] += 1
95+
elif status_code == 'A': stats["Added"] += 1
96+
elif status_code == 'D': stats["Deleted"] += 1
97+
elif status_code == 'R': stats["Renamed"] += 1
98+
99+
return file_list, stats
100+
101+
def generate_markdown(categorized, files, stats, commit_count):
102+
"""Builds the Markdown string."""
103+
today = datetime.date.today().strftime("%B %d, %Y")
104+
105+
md = f"# 🔄 Change Log - {today}\n\n"
106+
107+
md += "## 📊 Quick Stats\n"
108+
md += f"- **Commits Analyzed**: {commit_count}\n"
109+
md += f"- **Files Modified**: {stats['Modified']}\n"
110+
md += f"- **New Files**: {stats['Added']}\n"
111+
md += f"- **Deleted Files**: {stats['Deleted']}\n\n"
112+
113+
md += "## 📝 Detailed Changes\n\n"
114+
115+
for category, items in categorized.items():
116+
if items:
117+
md += f"### {category}\n"
118+
for item in items:
119+
md += f"{item}\n"
120+
md += "\n"
121+
122+
md += "## 📂 Files Touched\n"
123+
if len(files) > 0:
124+
md += "<details>\n<summary>Click to view full file list</summary>\n\n"
125+
for f in files:
126+
md += f"- `{f}`\n"
127+
md += "\n</details>"
128+
else:
129+
md += "*No files changed.*"
130+
131+
return md
132+
133+
def main():
134+
# Allow user to pass a number, e.g., python generate_changelog.py 5
135+
if len(sys.argv) > 1 and sys.argv[1].isdigit():
136+
num_commits = int(sys.argv[1])
137+
else:
138+
num_commits = DEFAULT_COMMITS_TO_SCAN
139+
140+
print(f"🔍 Scanning last {num_commits} commits...")
141+
142+
categorized_commits, count = parse_commits(num_commits)
143+
file_list, stats = get_files_changed(num_commits)
144+
145+
markdown_content = generate_markdown(categorized_commits, file_list, stats, count)
146+
147+
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
148+
f.write(markdown_content)
149+
150+
print(f"✅ Draft changelog generated: {OUTPUT_FILE}")
151+
print(" (Open this file, verify the details, and paste it into your main CHANGELOG.md)")
152+
153+
if __name__ == "__main__":
154+
main()

0 commit comments

Comments
 (0)