Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 14 additions & 130 deletions compendium/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
# License: MIT. See LICENSE

import importlib.util
import json
import os
import re
import subprocess
from collections import Counter
from urllib.parse import quote, urlencode

import frappe
from frappe import _
from frappe.translate import get_parent_language
from frappe.utils import cint, get_bench_path, get_url, has_common, sanitize_html
from frappe.utils import cint, get_url, has_common, sanitize_html
from frappe.utils.change_log import parse_github_url
from frappe.website.utils import extract_title, get_frontmatter

Expand Down Expand Up @@ -579,8 +577,8 @@ def get_edit_url(page):
if not owner or not repo:
return None

branch = get_app_git_branch(page.app)
if not branch or branch == "HEAD":
branch = get_app_docs_branch(page.app)
if not branch:
return None

relative_path = get_repo_relative_path(page)
Expand All @@ -593,141 +591,27 @@ def get_edit_url(page):


@frappe.request_cache
def get_app_repository_url(app):
"""Repository URL from the app's pyproject.toml `[project.urls]` Repository key."""
def get_app_pyproject(app):
"""Parsed pyproject.toml for an installed app, or an empty dict."""
from tomli import load

pyproject_path = os.path.join(os.path.dirname(get_app_path(app)), "pyproject.toml")
if not os.path.isfile(pyproject_path):
return None
return {}

with open(pyproject_path, "rb") as f:
data = load(f)

url = (data.get("project") or {}).get("urls", {}).get("Repository")
return url.rstrip("/") if url else None


@frappe.request_cache
def get_app_git_branch(app):
"""Git branch to use in GitHub links for this app.

Uses a ref from the git remote that matches `[project.urls].Repository`, so
forks and local-only branches do not produce 404s. Order: current branch if
on that remote → its upstream if on that remote → that remote's default.

Production images (Frappe Cloud, frappe_docker) strip `.git`, and remotes
may not point at GitHub. Then use the branch bench recorded at install.
"""
repo_root = os.path.dirname(get_app_path(app))
remote = _remote_for_repository(repo_root, get_app_repository_url(app))
if remote:
local = _git_output(repo_root, "rev-parse", "--abbrev-ref", "HEAD")
if local and local != "HEAD" and _git_ref_exists(repo_root, f"refs/remotes/{remote}/{local}"):
return local

upstream = _git_output(repo_root, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}")
tracking_remote, _, branch = upstream.partition("/")
if tracking_remote == remote and branch:
return branch
return load(f)

prefix = f"{remote}/"
default = _git_output(repo_root, "symbolic-ref", "--short", f"refs/remotes/{remote}/HEAD")
if default.startswith(prefix):
return default[len(prefix) :]

return get_recorded_app_branch(app)


def get_recorded_app_branch(app):
"""Branch recorded for this app when a live canonical git remote is unavailable."""
branch = get_apps_json_branch(app)
if branch:
return branch

recorded = frappe.db.get_value("Installed Application", {"app_name": app}, "git_branch")
if recorded and recorded not in ("UNVERSIONED", "HEAD"):
return recorded

return ""


def get_apps_json_branch(app):
"""Branch from sites/apps.json, written by bench get-app before `.git` is stripped."""
apps_json_path = os.path.join(get_bench_path(), "sites", "apps.json")
if not os.path.isfile(apps_json_path):
return ""

try:
with open(apps_json_path, encoding="utf-8") as f:
data = json.load(f)
except (OSError, ValueError):
return ""

if not isinstance(data, dict):
return ""

app_info = data.get(app) or {}
branch = (app_info.get("resolution") or {}).get("branch")
if branch and branch not in ("UNVERSIONED", "HEAD"):
return branch

return ""


def _git_output(repo_root, *args):
try:
with open(os.devnull, "wb") as null_stream:
result = subprocess.check_output(
["git", "-C", repo_root, *args],
shell=False,
stdin=null_stream,
stderr=null_stream,
)
except (OSError, subprocess.CalledProcessError):
return ""

return result.decode().strip()


def _git_ref_exists(repo_root, ref):
try:
with open(os.devnull, "wb") as null_stream:
subprocess.check_call(
["git", "-C", repo_root, "show-ref", "--verify", "--quiet", ref],
shell=False,
stdin=null_stream,
stdout=null_stream,
stderr=null_stream,
)
except (OSError, subprocess.CalledProcessError):
return False

return True


def _remote_for_repository(repo_root, repository):
"""Git remote whose URL points at the same GitHub owner/repo as `repository`."""
target = _github_repo(repository)
if not target:
return ""

for remote in _git_output(repo_root, "remote").split():
if _github_repo(_git_output(repo_root, "remote", "get-url", remote)) == target:
return remote
return ""
def get_app_repository_url(app):
"""Repository URL from the app's pyproject.toml `[project.urls]` Repository key."""
url = (get_app_pyproject(app).get("project") or {}).get("urls", {}).get("Repository")
return url.rstrip("/") if url else None


def _github_repo(url):
if not url:
return None
try:
owner, repo = parse_github_url(url)
except ValueError:
return None
if not owner or not repo:
return None
return (owner.lower(), repo.lower())
def get_app_docs_branch(app):
"""GitHub branch for Edit on GitHub, from `[tool.compendium] docs_branch`."""
return ((get_app_pyproject(app).get("tool") or {}).get("compendium") or {}).get("docs_branch") or ""


def get_repo_relative_path(page):
Expand Down
7 changes: 5 additions & 2 deletions compendium/docs/de/compendium/docs/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,16 @@ App bleiben.

## Auf GitHub bearbeiten

Wenn eine App unter `[project.urls]` in ihrer `pyproject.toml` eine GitHub-
`Repository`-URL angibt, sehen Nutzer mit der Rolle **Compendium Contributor**
Wenn eine App in ihrer `pyproject.toml` eine GitHub-`Repository`-URL und
`docs_branch` angibt, sehen Nutzer mit der Rolle **Compendium Contributor**
einen Link **Edit on GitHub**, der die Markdown-Datei der aktuellen Seite öffnet.

```toml
[project.urls]
Repository = "https://github.com/alyf-de/compendium.git"

[tool.compendium]
docs_branch = "version-15"
```

## Mermaid-Diagramme
Expand Down
5 changes: 4 additions & 1 deletion compendium/docs/en/compendium/docs/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,16 @@ Reference images with relative paths in Markdown. Compendium serves them through

## Edit on GitHub

When an app declares a GitHub `Repository` URL under `[project.urls]` in its
When an app declares a GitHub `Repository` URL and a `docs_branch` in its
`pyproject.toml`, contributors with the **Compendium Contributor** role see an
**Edit on GitHub** link that opens the Markdown file for the current page.

```toml
[project.urls]
Repository = "https://github.com/alyf-de/compendium.git"

[tool.compendium]
docs_branch = "version-15"
```

## Mermaid diagrams
Expand Down
Loading
Loading