1515from collections import OrderedDict
1616from os import path
1717from itertools import islice
18+ from textwrap import dedent
1819
1920from git import Repo
2021
22+ from docutils .parsers .rst import directives
23+
2124from sphinx .util import logging
2225from sphinx .config import ENUM
2326
2427from sphinxnotes .render import (
2528 extra_context ,
2629 ExtraContext ,
2730 ExtraContextRequest ,
31+ BaseContextDirective ,
32+ Phase ,
33+ Template ,
2834)
2935
3036from . import meta
@@ -156,7 +162,7 @@ def get_git_revisions(
156162 # :T: change in the type of the file
157163 # :U: file is unmerged (you must complete the merge before it can be committed)
158164 # :X: "unknown" change type (most probably a bug, please report it)
159- status_maps = {'M' : m , 'A' : a , 'D' : d }
165+ status_maps = {'M' : m , 'A' : a , 'D' : d }
160166
161167 # Use git diff --name-status with pathspecs for native pathspec matching
162168 name_status = repo .git .diff (
@@ -190,11 +196,98 @@ def get_git_revisions(
190196
191197
192198def path2doc (repo : Repo , env : BuildEnvironment , blob_path : str ) -> str | None :
193- """Convert a git repo-relative blob path to a Sphinx document name. """
199+ """Convert a git repo-relative blob path to a Sphinx document name."""
194200 docname = env .path2doc (path .join (repo .working_dir , blob_path ))
195201 return docname if (docname and not path .isabs (docname )) else None
196202
197203
204+ def collect_revisions (
205+ repo : Repo ,
206+ env : BuildEnvironment ,
207+ count : int ,
208+ paths : list [str ],
209+ group_by : str = '' ,
210+ ) -> list [Revision ]:
211+ """Collect recent revisions from git, optionally grouped by time period."""
212+ count = count or env .config .recentupdate_count
213+ group_by = group_by or env .config .recentupdate_group_by
214+
215+ git_revs = get_git_revisions (repo , env , paths )
216+
217+ if group_by :
218+ groups = OrderedDict ()
219+ for rev in git_revs :
220+ group_revisions (groups , rev , group_by )
221+ if len (groups ) >= count :
222+ break
223+ revs = compact_groups (groups )
224+ else :
225+ revs = list (islice (git_revs , count ))
226+ logger .info (
227+ f'[recentupdate] Expect { count } revisions, finally get { len (revs )} , group by { group_by } '
228+ )
229+
230+ return revs
231+
232+
233+ DEFAULT_TEMPLATE = dedent ("""\
234+ {% for r in revisions %}
235+ {{ r.date.strftime('%Y-%m-%d') }}
236+ :Author: {{ r.author }}
237+ :Message: {{ r.message | join(', ') }}
238+
239+ {% if r.changed_docs -%}
240+ - Modified {{ r.changed_docs | roles("doc") | join(", ") }}
241+ {% endif %}
242+ {% if r.added_docs -%}
243+ - Added {{ r.added_docs | roles("doc") | join(", ") }}
244+ {% endif %}
245+ {% if r.removed_docs -%}
246+ - Deleted {{ r.removed_docs | join(", ") }}
247+ {% endif %}
248+ {% endfor %}
249+ """ )
250+
251+
252+ class RecentUpdateDirective (BaseContextDirective ):
253+ """Directive for displaying recent document updates."""
254+
255+ has_content = True
256+ required_arguments = 0
257+ optional_arguments = 1
258+ option_spec = {
259+ 'self' : directives .flag ,
260+ 'paths' : directives .unchanged ,
261+ }
262+
263+ def current_context (self ) -> dict :
264+ repo = RecentUpdateExtraContext .repo
265+
266+ count = (
267+ int (self .arguments [0 ])
268+ if self .arguments
269+ else self .env .config .recentupdate_count
270+ )
271+
272+ current_doc = 'self' in self .options
273+ if current_doc :
274+ docpath = self .env .doc2path (self .env .docname )
275+ paths = [path .relpath (docpath , repo .working_dir )]
276+ elif 'paths' in self .options :
277+ paths = [p .strip () for p in self .options ['paths' ].splitlines () if p .strip ()]
278+ else :
279+ paths = ['.' ]
280+
281+ revs = collect_revisions (repo , self .env , count , paths )
282+ return {'revisions' : revs }
283+
284+ def current_template (self ) -> Template :
285+ text = '\n ' .join (self .content ) if self .has_content and self .content else ''
286+ if not text .strip ():
287+ text = self .env .config .recentupdate_template
288+ return Template (text , phase = Phase .Parsing )
289+
290+
198291@extra_context ('recentupdate' )
199292class RecentUpdateExtraContext (ExtraContext ):
200293 """Extra context providing recent document revisions from Git."""
@@ -206,34 +299,18 @@ def generate(
206299 self ,
207300 req : ExtraContextRequest ,
208301 count : int = 0 ,
209- paths : list [str ] = ['.' , ],
302+ paths : list [str ] = [
303+ '.' ,
304+ ],
210305 current_doc : bool = False ,
211306 group_by : str = '' ,
212307 ) -> Any :
213- count = count or req .env .config .recentupdate_count
214- group_by = group_by or req .env .config .recentupdate_group_by
215-
216308 if current_doc :
217309 docpath = req .env .doc2path (req .env .docname )
218310 repo_path = path .relpath (docpath , self .repo .working_dir )
219311 paths = [repo_path ]
220312
221- git_revs = get_git_revisions (self .repo , req .env , paths )
222-
223- if group_by :
224- groups = OrderedDict ()
225- for rev in git_revs :
226- group_revisions (groups , rev , group_by )
227- if len (groups ) >= count :
228- break
229- revs = compact_groups (groups )
230- else :
231- revs = list (islice (git_revs , count ))
232- logger .info (
233- f'[recentupdate] Expect { count } revisions, finally get { len (revs )} , group by { group_by } '
234- )
235-
236- return revs
313+ return collect_revisions (self .repo , req .env , count , paths , group_by )
237314
238315
239316def setup (app : Sphinx ):
@@ -243,10 +320,13 @@ def setup(app: Sphinx):
243320
244321 app .setup_extension ('sphinxnotes.render' )
245322
323+ app .add_directive ('recentupdate' , RecentUpdateDirective )
324+
246325 app .add_config_value (
247326 'recentupdate_exclude_commit' , ['skip-recentupdate' ], 'env' , types = list [str ]
248327 )
249328 app .add_config_value ('recentupdate_count' , 10 , 'env' , types = int )
329+ app .add_config_value ('recentupdate_template' , DEFAULT_TEMPLATE , 'env' , types = str )
250330 app .add_config_value (
251331 'recentupdate_group_by' , None , 'env' , types = ENUM (None , 'day' , 'month' , 'year' )
252332 )
0 commit comments