1# Copyright (c) 2022 Anton Bobkov, Intel Corporation
2# SPDX-License-Identifier: Apache-2.0
3
4# Use git to retrieve a more meaningful last updated date than the usual
5# Sphinx-generated date that's actually just the last published date.
6# Inspired by a shell script by David Kinder.
7#
8# Add the extension to your conf.py file:
9#    extensions = ['last_updated']
10#
11# If you copy documentation files from one or multiple git repositories prior to
12# running Sphinx, specify the list of paths to the git folders in the
13# last_updated_git_path parameter. You can either specify absolute paths or
14# relative paths from the documentation root directory in this list. For
15# example:
16#
17#   last_updated_git_path = ['../../', '../../../']
18#
19# Specify the date format in the html_last_updated_fmt parameter. For
20# information about the strftime() format, see https://strftime.org.  If you do
21# not specify this parameter, the extension uses the default value of "%b %d, %Y"
22# for short month name, date, and four-digit year.
23#
24# Use the variables provided by the extension in your jinja templates:
25#
26#   last_updated. The date of the last update of the rst file in the specified
27#      git repository.
28#   last_published. The publication date of the rst file.
29#
30# Note: Sphinx already provides the last_updated variable. However, this
31# variable includes the publication time. This extension overwrites the
32# last_updated variable with the file modification time in git.
33#
34# To override the default footer in the sphinx_rtd_theme, create the footer.html
35# file in the _templates directory:
36#
37# {% extends "!footer.html" %}
38# {% block contentinfo %}
39#
40# <!-- your copyright info goes here, possibly copied from the theme's # footer.html -->
41#
42# <span class="lastupdated">Last updated on {{last_updated}}. Published on {{last_published}}</span>
43#
44# {% endblock %}
45#
46# This snippet overrides the contentinfo block of the default footer.html
47# template that initially contains the information about the copyright and
48# publication date.
49
50__version__ = '0.1.0'
51
52import subprocess
53from datetime import date, datetime
54import os
55import urllib.parse
56from sphinx.util import logging
57
58
59def _not_git_repo(dir):
60    res = subprocess.call(['git', '-C', dir, 'rev-parse'],
61            stderr=subprocess.STDOUT, stdout = open(os.devnull, 'w')) != 0
62    return res
63
64
65def _get_last_updated_from_git(file_path, git_repo, doc_root):
66
67    rel_path = os.path.relpath(file_path, doc_root)
68    time_format = "%Y-%m-%d"
69
70    for git_repo_path in git_repo:
71
72        new_path = os.path.join(git_repo_path, rel_path)
73        if os.path.isfile(new_path):
74            try:
75                    output=subprocess.check_output(
76                        f'git --no-pager log -1 --date=format:"{time_format}" --pretty="format:%cd" {new_path}',
77                        shell=True, cwd=git_repo_path)
78            except:
79                    # Could not get git info for an existing file, try the next
80                    # folder on the list
81                    continue
82            else:
83                # we found the .rst file in one of the git_repo paths, either
84                # use the date of the last commit (from git) or today's date if
85                # there is no git history for this file.
86                try:
87                    last_updated = datetime.strptime(output.decode('utf-8'), time_format).date()
88                    return last_updated
89                except:
90                    return date.today()
91        else:
92            # can't find a .rst file on that git_repo path, try the next
93            continue
94
95    # falling out of the loop means we can't find that file in any of the
96    # git_repo paths
97    return None
98
99
100def on_html_page_context(app, pagename, templatename, context, doctree):
101    if doctree:
102
103        # If last_updated_git_path (with a list of potential folders where the
104        # actual git-managed files are) is not specified,
105        # then just use the doc source path
106        if app.config.last_updated_git_path is None:
107            app.config.last_updated_git_path = [app.srcdir]
108
109        # If last_updated_git_path is a relative path, convert it to absolute
110        last_updated_git_path_abs = []
111        for last_updated_git_path_el in app.config.last_updated_git_path:
112            if not os.path.isabs(last_updated_git_path_el):
113                last_updated_git_path_el_abs = os.path.normpath(os.path.join(app.srcdir, last_updated_git_path_el))
114                last_updated_git_path_abs.append(last_updated_git_path_el_abs)
115            else:
116                last_updated_git_path_abs.append(last_updated_git_path_el)
117
118            if _not_git_repo(last_updated_git_path_abs[-1]):
119                app.logger.error(f"The last_updated extension is disabled because of the error:\
120                        \n {last_updated_git_path_abs} is not a git repository.\
121                        \n Specify correct path(s) to the git source folder(s) in last_updated_git_path.")
122                app.disconnect(app.listener_id)
123                return
124
125        app.config.last_updated_git_path = last_updated_git_path_abs
126
127
128        # Get the absolute path to the current rst document
129        rst_file_path = doctree.attributes['source']
130
131        # Set the date format based on html_last_updated_fmt or default of Mar 18, 2022
132        if app.config.html_last_updated_fmt is None:
133            date_fmt = "%b %d, %Y"
134        else:
135            date_fmt = app.config.html_last_updated_fmt
136
137        context['last_published'] = date.today().strftime(date_fmt)
138
139        last_updated_value = _get_last_updated_from_git(file_path=rst_file_path,
140                                                        git_repo=app.config.last_updated_git_path,
141                                                        doc_root=app.srcdir)
142        if last_updated_value is None:
143            app.logger.warning(f'Could not get the last updated value from git for the following file:\
144                    \n {rst_file_path}\n Ensure that you specified the correct folder(s) in last_updated_git_path:\
145                    \n {app.config.last_updated_git_path}\n')
146            context['last_updated'] = context['last_published']
147        else:
148            context['last_updated'] = last_updated_value.strftime(date_fmt)
149
150
151
152def setup(app):
153    app.logger = logging.getLogger(__name__)
154    app.add_config_value('last_updated_git_path', None, 'html')
155
156    app.listener_id = app.connect('html-page-context', on_html_page_context)
157
158    return {
159            'version': '0.1',
160            'parallel_read_safe': True,
161    }
162