1""" 2Git/GitHub utilities for Sphinx 3############################### 4 5Copyright (c) 2021 Nordic Semiconductor ASA 6Copyright (c) 2023 The Linux Foundation 7 8SPDX-License-Identifier: Apache-2.0 9 10Introduction 11============ 12 13This Sphinx extension can be used to obtain various Git and GitHub related metadata for a page. 14This is useful, for example, when adding features like "Open on GitHub" on top 15of pages, direct links to open a GitHub issue regarding a page, or date of the most recent commit 16to a page. 17 18The extension installs the following Jinja filter: 19 20* ``gh_link_get_blob_url``: Returns a URL to the source of a page on GitHub. 21* ``gh_link_get_edit_url``: Returns a URL to edit the given page on GitHub. 22* ``gh_link_get_open_issue_url``: Returns a URL to open a new issue regarding the given page. 23* ``git_info``: Returns the date and SHA1 of the last commit made to a page (if this page is 24 managed by Git). 25 26Configuration options 27===================== 28 29- ``gh_link_version``: GitHub version to use in the URL (e.g. "main") 30- ``gh_link_base_url``: Base URL used as a prefix for generated URLs. 31- ``gh_link_prefixes``: Mapping of pages (regex) <> GitHub prefix. 32- ``gh_link_exclude``: List of pages (regex) that will not report a URL. Useful 33 for, e.g., auto-generated pages not in Git. 34""" 35 36import os 37import re 38import subprocess 39import sys 40from datetime import datetime 41from functools import partial 42from pathlib import Path 43from typing import Final 44from urllib.parse import urlencode 45 46from sphinx.application import Sphinx 47from sphinx.util.i18n import format_date 48 49sys.path.insert(0, str(Path(__file__).parents[3] / "scripts")) 50 51from get_maintainer import Maintainers 52 53ZEPHYR_BASE: Final[str] = Path(__file__).parents[3] 54MAINTAINERS: Final[Maintainers] = Maintainers(filename=f"{ZEPHYR_BASE}/MAINTAINERS.yml") 55 56 57__version__ = "0.1.0" 58 59 60def get_page_prefix(app: Sphinx, pagename: str) -> str: 61 """Return the prefix that needs to be added to the page path to get its location in the 62 repository. 63 64 If pagename refers to a page that is automatically generated by Sphinx or if it matches one of 65 the patterns in ``gh_link_exclude`` configuration option, return None. 66 67 Args: 68 app: Sphinx instance. 69 pagename: Page name (path). 70 71 Returns: 72 Prefix if applicable, None otherwise. 73 """ 74 75 if not os.path.isfile(app.env.doc2path(pagename)): 76 return None 77 78 for exclude in app.config.gh_link_exclude: 79 if re.match(exclude, pagename): 80 return None 81 82 found_prefix = "" 83 for pattern, prefix in app.config.gh_link_prefixes.items(): 84 if re.match(pattern, pagename): 85 found_prefix = prefix 86 break 87 88 return found_prefix 89 90 91def gh_link_get_url(app: Sphinx, pagename: str, mode: str = "blob") -> str | None: 92 """Obtain GitHub URL for the given page. 93 94 Args: 95 app: Sphinx instance. 96 mode: Typically "edit", or "blob". 97 pagename: Page name (path). 98 99 Returns: 100 GitHub URL if applicable, None otherwise. 101 """ 102 103 page_prefix = get_page_prefix(app, pagename) 104 if page_prefix is None: 105 return None 106 107 return "/".join( 108 [ 109 app.config.gh_link_base_url, 110 mode, 111 app.config.gh_link_version, 112 page_prefix, 113 str(app.env.doc2path(pagename, False)), 114 ] 115 ) 116 117 118def gh_link_get_open_issue_url(app: Sphinx, pagename: str, sha1: str) -> str | None: 119 """Link to open a new Github issue regarding "pagename" using the bug report template. 120 121 Args: 122 app: Sphinx instance. 123 pagename: Page name (path). 124 sha1: SHA1 of the last commit to the page. 125 126 Returns: 127 URL to open a new issue if applicable, None otherwise. 128 """ 129 130 page_prefix = get_page_prefix(app, pagename) 131 if page_prefix is None: 132 return None 133 134 rel_path = os.path.join( 135 os.path.relpath(ZEPHYR_BASE), 136 page_prefix, 137 app.env.doc2path(pagename, False), 138 ) 139 140 form_data = { 141 "template": "001_bug_report.yml", 142 "title": f"doc: Documentation issue in '{pagename}'", 143 "labels": ["bug", "area: Documentation"], 144 "env": (f"- Page: {pagename}\n- Version: {app.config.gh_link_version}\n- SHA-1: {sha1}"), 145 "context": ( 146 "This issue was reported from the online documentation page using the " 147 "'Report an issue' button." 148 ), 149 } 150 151 areas = MAINTAINERS.path2areas(rel_path) 152 if areas: 153 for area in areas: 154 form_data["labels"].extend(area.labels) 155 form_data["labels"] = ",".join(form_data.get("labels", [])) 156 157 base_url = f"{app.config.gh_link_base_url}/issues/new" 158 return f"{base_url}?{urlencode(form_data)}" 159 160 161def git_info_filter(app: Sphinx, pagename) -> tuple[str, str] | None: 162 """Return a tuple with the date and SHA1 of the last commit made to a page. 163 164 Arguments: 165 app {Sphinx} -- Sphinx application object 166 pagename {str} -- Page name 167 168 Returns: 169 Optional[Tuple[str, str]] -- Tuple with the date and SHA1 of the last commit made to the 170 page, or None if the page is not in the repo (generated file, or manually authored file not 171 yet tracked by git). 172 """ 173 174 page_prefix = get_page_prefix(app, pagename) 175 if page_prefix is None: 176 return None 177 178 orig_path = os.path.join( 179 ZEPHYR_BASE, 180 page_prefix, 181 app.env.doc2path(pagename, False), 182 ) 183 184 # Check if the file is tracked by git 185 try: 186 subprocess.check_output( 187 ["git", "ls-files", "--error-unmatch", orig_path], 188 stderr=subprocess.STDOUT, 189 ) 190 except subprocess.CalledProcessError: 191 return None 192 193 try: 194 date_and_sha1 = ( 195 subprocess.check_output( 196 [ 197 "git", 198 "log", 199 "-1", 200 "--format=%ad %H", 201 "--date=unix", 202 orig_path, 203 ], 204 stderr=subprocess.STDOUT, 205 ) 206 .decode("utf-8") 207 .strip() 208 ) 209 if not date_and_sha1: # added but not committed 210 return None 211 date, sha1 = date_and_sha1.split(" ", 1) 212 date_object = datetime.fromtimestamp(int(date)) 213 last_update_fmt = app.config.html_last_updated_fmt 214 if last_update_fmt is not None: 215 date = format_date(last_update_fmt, date=date_object, language=app.config.language) 216 217 return (date, sha1) 218 except subprocess.CalledProcessError: 219 return None 220 221 222def add_jinja_filter(app: Sphinx): 223 if app.builder.format != "html": 224 return 225 226 app.builder.templates.environment.filters["gh_link_get_blob_url"] = partial( 227 gh_link_get_url, app, mode="blob" 228 ) 229 230 app.builder.templates.environment.filters["gh_link_get_edit_url"] = partial( 231 gh_link_get_url, app, mode="edit" 232 ) 233 234 app.builder.templates.environment.filters["gh_link_get_open_issue_url"] = partial( 235 gh_link_get_open_issue_url, app 236 ) 237 238 app.builder.templates.environment.filters["git_info"] = partial(git_info_filter, app) 239 240 241def setup(app: Sphinx): 242 app.add_config_value("gh_link_version", "", "") 243 app.add_config_value("gh_link_base_url", "", "") 244 app.add_config_value("gh_link_prefixes", {}, "") 245 app.add_config_value("gh_link_exclude", [], "") 246 247 app.connect("builder-inited", add_jinja_filter) 248 249 return { 250 "version": __version__, 251 "parallel_read_safe": True, 252 "parallel_write_safe": True, 253 } 254