2023-10-25 12:12:02 +02:00
|
|
|
"""
|
|
|
|
Git/GitHub utilities for Sphinx
|
|
|
|
###############################
|
|
|
|
|
|
|
|
Copyright (c) 2021 Nordic Semiconductor ASA
|
|
|
|
Copyright (c) 2023 The Linux Foundation
|
|
|
|
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
Introduction
|
|
|
|
============
|
|
|
|
|
|
|
|
This Sphinx extension can be used to obtain various Git and GitHub related metadata for a page.
|
|
|
|
This is useful, for example, when adding features like "Open on GitHub" on top
|
|
|
|
of pages, direct links to open a GitHub issue regarding a page, or date of the most recent commit
|
|
|
|
to a page.
|
|
|
|
|
|
|
|
The extension installs the following Jinja filter:
|
|
|
|
|
|
|
|
* ``gh_link_get_blob_url``: Returns a URL to the source of a page on GitHub.
|
|
|
|
* ``gh_link_get_edit_url``: Returns a URL to edit the given page on GitHub.
|
|
|
|
* ``gh_link_get_open_issue_url``: Returns a URL to open a new issue regarding the given page.
|
|
|
|
* ``git_info``: Returns the date and SHA1 of the last commit made to a page (if this page is
|
|
|
|
managed by Git).
|
|
|
|
|
|
|
|
Configuration options
|
|
|
|
=====================
|
|
|
|
|
|
|
|
- ``gh_link_version``: GitHub version to use in the URL (e.g. "main")
|
|
|
|
- ``gh_link_base_url``: Base URL used as a prefix for generated URLs.
|
|
|
|
- ``gh_link_prefixes``: Mapping of pages (regex) <> GitHub prefix.
|
|
|
|
- ``gh_link_exclude``: List of pages (regex) that will not report a URL. Useful
|
|
|
|
for, e.g., auto-generated pages not in Git.
|
|
|
|
"""
|
|
|
|
|
|
|
|
from functools import partial
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import subprocess
|
2023-11-03 16:21:58 +01:00
|
|
|
import sys
|
2023-10-25 12:12:02 +02:00
|
|
|
from datetime import datetime
|
|
|
|
from pathlib import Path
|
|
|
|
from textwrap import dedent
|
2023-11-03 15:43:54 +01:00
|
|
|
from typing import Final, Optional, Tuple
|
2023-10-25 12:12:02 +02:00
|
|
|
from urllib.parse import quote
|
|
|
|
|
|
|
|
from sphinx.application import Sphinx
|
|
|
|
from sphinx.util.i18n import format_date
|
|
|
|
|
2023-11-03 15:43:54 +01:00
|
|
|
ZEPHYR_BASE : Final[str] = Path(__file__).parents[3]
|
2023-11-03 16:21:58 +01:00
|
|
|
SCRIPTS : Final[str] = ZEPHYR_BASE / "scripts"
|
|
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
|
|
|
|
|
|
from get_maintainer import Maintainers
|
|
|
|
|
2023-12-04 23:26:17 +01:00
|
|
|
MAINTAINERS : Final[Maintainers] = Maintainers(filename=f"{ZEPHYR_BASE}/MAINTAINERS.yml")
|
2023-11-03 16:21:58 +01:00
|
|
|
|
2023-10-25 12:12:02 +02:00
|
|
|
|
|
|
|
__version__ = "0.1.0"
|
|
|
|
|
|
|
|
|
|
|
|
def get_page_prefix(app: Sphinx, pagename: str) -> str:
|
2023-11-03 15:44:39 +01:00
|
|
|
"""Return the prefix that needs to be added to the page path to get its location in the
|
|
|
|
repository.
|
|
|
|
|
|
|
|
If pagename refers to a page that is automatically generated by Sphinx or if it matches one of
|
|
|
|
the patterns in ``gh_link_exclude`` configuration option, return None.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
app: Sphinx instance.
|
|
|
|
pagename: Page name (path).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Prefix if applicable, None otherwise.
|
|
|
|
"""
|
|
|
|
|
2023-11-03 11:33:36 +01:00
|
|
|
if not os.path.isfile(app.env.doc2path(pagename)):
|
2023-10-25 12:12:02 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
for exclude in app.config.gh_link_exclude:
|
|
|
|
if re.match(exclude, pagename):
|
|
|
|
return None
|
|
|
|
|
|
|
|
found_prefix = ""
|
|
|
|
for pattern, prefix in app.config.gh_link_prefixes.items():
|
|
|
|
if re.match(pattern, pagename):
|
|
|
|
found_prefix = prefix
|
|
|
|
break
|
|
|
|
|
|
|
|
return found_prefix
|
|
|
|
|
|
|
|
|
|
|
|
def gh_link_get_url(app: Sphinx, pagename: str, mode: str = "blob") -> Optional[str]:
|
|
|
|
"""Obtain GitHub URL for the given page.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
app: Sphinx instance.
|
|
|
|
mode: Typically "edit", or "blob".
|
|
|
|
pagename: Page name (path).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
GitHub URL if applicable, None otherwise.
|
|
|
|
"""
|
|
|
|
|
|
|
|
page_prefix = get_page_prefix(app, pagename)
|
2023-11-03 12:06:45 +01:00
|
|
|
if page_prefix is None:
|
2023-10-25 12:12:02 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
return "/".join(
|
|
|
|
[
|
|
|
|
app.config.gh_link_base_url,
|
|
|
|
mode,
|
|
|
|
app.config.gh_link_version,
|
|
|
|
page_prefix,
|
2023-11-03 11:33:36 +01:00
|
|
|
app.env.doc2path(pagename, False),
|
2023-10-25 12:12:02 +02:00
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def gh_link_get_open_issue_url(app: Sphinx, pagename: str, sha1: str) -> Optional[str]:
|
|
|
|
"""Link to open a new Github issue regarding "pagename" with title, body, and
|
|
|
|
labels already pre-filled with useful information.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
app: Sphinx instance.
|
|
|
|
pagename: Page name (path).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
URL to open a new issue if applicable, None otherwise.
|
|
|
|
"""
|
|
|
|
|
2023-11-03 16:21:58 +01:00
|
|
|
page_prefix = get_page_prefix(app, pagename)
|
|
|
|
if page_prefix is None:
|
2023-10-25 12:12:02 +02:00
|
|
|
return None
|
|
|
|
|
2023-11-03 16:21:58 +01:00
|
|
|
rel_path = os.path.join(
|
|
|
|
os.path.relpath(ZEPHYR_BASE),
|
|
|
|
page_prefix,
|
|
|
|
app.env.doc2path(pagename, False),
|
|
|
|
)
|
|
|
|
|
2023-11-03 15:25:40 +01:00
|
|
|
title = quote(f"doc: Documentation issue in '{pagename}'")
|
2023-10-25 12:12:02 +02:00
|
|
|
labels = quote("area: Documentation")
|
2023-11-03 16:21:58 +01:00
|
|
|
areas = MAINTAINERS.path2areas(rel_path)
|
|
|
|
if areas:
|
|
|
|
labels += "," + ",".join([label for area in areas for label in area.labels])
|
2023-10-25 12:12:02 +02:00
|
|
|
body = quote(
|
|
|
|
dedent(
|
|
|
|
f"""\
|
|
|
|
**Describe the bug**
|
|
|
|
|
|
|
|
<< Please describe the issue here >>
|
|
|
|
<< You may also want to update the automatically generated issue title above. >>
|
|
|
|
|
|
|
|
**Environment**
|
|
|
|
|
|
|
|
* Page: `{pagename}`
|
|
|
|
* Version: {app.config.gh_link_version}
|
|
|
|
* SHA-1: {sha1}
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return f"{app.config.gh_link_base_url}/issues/new?title={title}&labels={labels}&body={body}"
|
|
|
|
|
|
|
|
|
|
|
|
def git_info_filter(app: Sphinx, pagename) -> Optional[Tuple[str, str]]:
|
|
|
|
"""Return a tuple with the date and SHA1 of the last commit made to a page.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
app {Sphinx} -- Sphinx application object
|
|
|
|
pagename {str} -- Page name
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Optional[Tuple[str, str]] -- Tuple with the date and SHA1 of the last commit made to the
|
2023-11-23 14:02:19 +01:00
|
|
|
page, or None if the page is not in the repo (generated file, or manually authored file not
|
|
|
|
yet tracked by git).
|
2023-10-25 12:12:02 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
page_prefix = get_page_prefix(app, pagename)
|
2023-11-03 12:06:45 +01:00
|
|
|
if page_prefix is None:
|
2023-10-25 12:12:02 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
orig_path = os.path.join(
|
2023-11-03 15:43:54 +01:00
|
|
|
ZEPHYR_BASE,
|
2023-10-25 12:12:02 +02:00
|
|
|
page_prefix,
|
2023-11-03 11:33:36 +01:00
|
|
|
app.env.doc2path(pagename, False),
|
2023-10-25 12:12:02 +02:00
|
|
|
)
|
|
|
|
|
2023-11-23 14:02:19 +01:00
|
|
|
# Check if the file is tracked by git
|
|
|
|
try:
|
|
|
|
subprocess.check_output(
|
|
|
|
["git", "ls-files", "--error-unmatch", orig_path],
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
)
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
return None
|
|
|
|
|
2023-10-25 12:12:02 +02:00
|
|
|
try:
|
|
|
|
date_and_sha1 = (
|
|
|
|
subprocess.check_output(
|
|
|
|
[
|
|
|
|
"git",
|
|
|
|
"log",
|
|
|
|
"-1",
|
|
|
|
"--format=%ad %H",
|
|
|
|
"--date=unix",
|
|
|
|
orig_path,
|
|
|
|
],
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
)
|
|
|
|
.decode("utf-8")
|
|
|
|
.strip()
|
|
|
|
)
|
|
|
|
date, sha1 = date_and_sha1.split(" ", 1)
|
|
|
|
date_object = datetime.fromtimestamp(int(date))
|
|
|
|
last_update_fmt = app.config.html_last_updated_fmt
|
|
|
|
if last_update_fmt is not None:
|
|
|
|
date = format_date(last_update_fmt, date=date_object, language=app.config.language)
|
|
|
|
|
|
|
|
return (date, sha1)
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def add_jinja_filter(app: Sphinx):
|
|
|
|
if app.builder.format != "html":
|
|
|
|
return
|
|
|
|
|
|
|
|
app.builder.templates.environment.filters["gh_link_get_blob_url"] = partial(
|
|
|
|
gh_link_get_url, app, mode="blob"
|
|
|
|
)
|
|
|
|
|
|
|
|
app.builder.templates.environment.filters["gh_link_get_edit_url"] = partial(
|
|
|
|
gh_link_get_url, app, mode="edit"
|
|
|
|
)
|
|
|
|
|
|
|
|
app.builder.templates.environment.filters["gh_link_get_open_issue_url"] = partial(
|
|
|
|
gh_link_get_open_issue_url, app
|
|
|
|
)
|
|
|
|
|
|
|
|
app.builder.templates.environment.filters["git_info"] = partial(git_info_filter, app)
|
|
|
|
|
|
|
|
|
|
|
|
def setup(app: Sphinx):
|
|
|
|
app.add_config_value("gh_link_version", "", "")
|
|
|
|
app.add_config_value("gh_link_base_url", "", "")
|
|
|
|
app.add_config_value("gh_link_prefixes", {}, "")
|
|
|
|
app.add_config_value("gh_link_exclude", [], "")
|
|
|
|
|
|
|
|
app.connect("builder-inited", add_jinja_filter)
|
|
|
|
|
|
|
|
return {
|
|
|
|
"version": __version__,
|
|
|
|
"parallel_read_safe": True,
|
|
|
|
"parallel_write_safe": True,
|
|
|
|
}
|