Skip to content

Commit b91cb66

Browse files
committed
Automate README generation
1 parent c42801b commit b91cb66

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

.github/workflows/build.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Build README
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Check out repo
13+
uses: actions/checkout@v3
14+
# We need full history to introspect created/updated:
15+
with:
16+
fetch-depth: 0
17+
- name: Set up Python
18+
uses: actions/setup-python@v4
19+
with:
20+
python-version: '3.10'
21+
- uses: actions/cache@v3
22+
name: Configure pip caching
23+
with:
24+
path: ~/.cache/pip
25+
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
26+
restore-keys: |
27+
${{ runner.os }}-pip-
28+
- name: Install Python dependencies
29+
run: |
30+
python -m pip install --upgrade pip
31+
pip install -r requirements.txt
32+
- name: Generate README
33+
run: |-
34+
python generate_readme.py --tmpl README.adoc.j2 --out README.adoc
35+
cat README.adoc
36+
- name: Commit and push if README changed
37+
run: |-
38+
git diff
39+
git config --global user.email "[email protected]"
40+
git config --global user.name "readme-gen-bot"
41+
git diff --quiet || (git add README.adoc && git commit -m "Update README")
42+
git push

README.adoc.j2

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
= Today I Learned... (TIL)
2+
3+
Inspired from https://github.com/simonw/til[simonw's TILs] and discovered through his https://simonwillison.net/2020/Jul/10/self-updating-profile-readme/[self-update README blog post].
4+
5+
As per jbranchaud definition, TILs are:
6+
7+
[quote, jbranchaud, https://github.com/jbranchaud/til]
8+
A collection of concise write-ups on small things I learn day to day across a variety of languages and technologies. These are things that don't really warrant a full blog post.
9+
10+
== My TILs
11+
12+
{{ til.count }} TILs so far
13+
14+
=== Topics
15+
16+
{% for topic in til.topics -%}
17+
* <<{{topic.anchor}},{{topic.title}}>> ({{topic.count}})
18+
{% endfor %}
19+
{%- for topic in til.topics %}
20+
=== {{topic.title}} [[{{topic.anchor}}]]
21+
22+
{% for entry in topic.entries -%}
23+
* link:{{entry.link}}[{{entry.title}}] _(updated: {{entry.updated}})_
24+
{% endfor -%}
25+
{% endfor %}
26+
== Other interesting TILs
27+
28+
* https://github.com/simonw/til
29+
* https://github.com/jbranchaud/til
30+
* https://github.com/jwworth/til
31+
* https://github.com/thoughtbot/til

generate_readme.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import sys
2+
3+
if not sys.version_info > (2, 7):
4+
print("Python 2 is no more supported")
5+
exit(-1)
6+
elif not sys.version_info >= (3, 8):
7+
print("Requires at least python 3.8 (using dataclasses and walrus operator)")
8+
exit(-1)
9+
10+
import dataclasses
11+
from dataclasses import dataclass
12+
from typing import List
13+
from datetime import datetime
14+
import argparse
15+
from pathlib import Path
16+
import git
17+
import re
18+
import copy
19+
import json
20+
from jinja2 import Environment, FileSystemLoader
21+
22+
import pprint
23+
24+
PATTERN_TIL_ENTRY_FILE = re.compile(".*\\.(md|adoc)")
25+
PATTERN_TIL_ENTRY_RENAME = re.compile("^(.*) => (.*)$")
26+
PATTERN_TIL_ENTRY_MOVE = re.compile("^{(.*) => (.*)}(.*)$")
27+
PATTERN_TIL_ENTRY_TITLE = re.compile("^= (.*)$")
28+
29+
@dataclass
30+
class TILTimeline:
31+
created: datetime
32+
updated: datetime
33+
34+
@dataclass
35+
class TILEntry:
36+
title: str
37+
link: str
38+
created: datetime
39+
updated: datetime
40+
41+
@dataclass
42+
class TILTopic:
43+
title: str
44+
anchor: str
45+
count: int
46+
entries: List[TILEntry]
47+
48+
@dataclass
49+
class TILInfos:
50+
count: int
51+
topics: List[TILTopic]
52+
53+
def update_til_entry_path(til_timeline, old, new):
54+
til_timeline[new] = copy.deepcopy(til_timeline[old])
55+
return new
56+
57+
def create_til_timeline(repo_path, ref='main'):
58+
til_timeline = {}
59+
repo = git.Repo(repo_path, odbt=git.GitDB)
60+
commits = reversed(list(repo.iter_commits(ref)))
61+
for commit in commits:
62+
dt = commit.authored_datetime
63+
files = list(commit.stats.files.keys())
64+
for file_path in files:
65+
if match_file := PATTERN_TIL_ENTRY_FILE.match(file_path):
66+
if match_move := PATTERN_TIL_ENTRY_MOVE.match(file_path):
67+
til_entry_path = update_til_entry_path(til_timeline,
68+
old = match_move.group(1)+match_move.group(3),
69+
new = match_move.group(2)+match_move.group(3))
70+
elif match_rename := PATTERN_TIL_ENTRY_RENAME.match(file_path):
71+
til_entry_path = update_til_entry_path(til_timeline,
72+
old = match_rename.group(1),
73+
new = match_rename.group(2))
74+
else:
75+
til_entry_path = file_path
76+
if til_entry_path not in til_timeline:
77+
til_timeline[til_entry_path] = TILTimeline(created=dt, updated=dt)
78+
else:
79+
til_timeline[til_entry_path].updated = dt
80+
return til_timeline
81+
82+
def prepare_til_infos(git_repo_path, til_timeline):
83+
til_infos = TILInfos(count=0, topics=[])
84+
topics = (topic for topic in Path(git_repo_path).iterdir() if topic.is_dir() and not topic.name.startswith('.'))
85+
for topic in topics:
86+
til_topic = TILTopic(title=topic.name, anchor=topic.name.lower(), count=0, entries=[])
87+
til_infos.topics.append(til_topic)
88+
entries = (entry for entry in topic.iterdir() if PATTERN_TIL_ENTRY_FILE.match(entry.name))
89+
for entry in entries:
90+
with open(entry, 'r') as f:
91+
line = f.readline()
92+
if match_entry_title := PATTERN_TIL_ENTRY_TITLE.match(line):
93+
title = match_entry_title.group(1)
94+
else:
95+
continue
96+
link=str(entry.relative_to(topic.parent))
97+
til_entry = TILEntry(title=title, link=link, created=til_timeline[link].created.date().isoformat(), updated=til_timeline[link].updated.date().isoformat())
98+
til_topic.entries.append(til_entry)
99+
til_topic.count += 1
100+
til_infos.count += 1
101+
return dataclasses.asdict(til_infos)
102+
103+
def render_readme_template(git_repo_path, template, til_infos):
104+
jinja2env = Environment(loader=FileSystemLoader(searchpath=git_repo_path))
105+
readme_template = jinja2env.get_template(template)
106+
return readme_template.render(til=til_infos)
107+
108+
def generate_readme(git_repo_path, template, output):
109+
til_timeline = create_til_timeline(git_repo_path)
110+
til_infos = prepare_til_infos(git_repo_path, til_timeline)
111+
with open(output, 'w') as f:
112+
f.write(render_readme_template(git_repo_path, template, til_infos))
113+
114+
def main():
115+
git_repo_path = Path(__file__).parent.resolve()
116+
117+
parser = argparse.ArgumentParser(description='Generate README')
118+
parser.add_argument('--tmpl', metavar='template', type=str, help='name of the template')
119+
parser.add_argument('--out', metavar='output', type=str, help='name of the README file')
120+
args = parser.parse_args()
121+
122+
generate_readme(git_repo_path, args.tmpl, args.out)
123+
124+
if __name__ == "__main__":
125+
main()

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GitPython==3.1.30
2+
Jinja2==3.1.2

0 commit comments

Comments
 (0)