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