diff --git a/custom_components/bahmcloud_store/providers.py b/custom_components/bahmcloud_store/providers.py index ca2007f..ccb005f 100644 --- a/custom_components/bahmcloud_store/providers.py +++ b/custom_components/bahmcloud_store/providers.py @@ -504,4 +504,160 @@ async def fetch_readme_markdown( except Exception: continue - return None \ No newline at end of file + return None + + +async def fetch_repo_versions( + hass: HomeAssistant, + repo_url: str, + *, + provider: str | None = None, + default_branch: str | None = None, + limit: int = 20, +) -> list[dict[str, str]]: + """List available versions/refs for a repository. + + Returns a list of dicts with keys: + - ref: the ref to install (tag/release/branch) + - label: human-friendly label + - source: release|tag|branch + + Notes: + - Uses public endpoints (no tokens) for public repositories. + - We prefer releases first (if available), then tags. + """ + + repo_url = (repo_url or "").strip() + if not repo_url: + return [] + + prov = (provider or "").strip().lower() if provider else "" + if not prov: + prov = detect_provider(repo_url) + + owner, repo = _split_owner_repo(repo_url) + if not owner or not repo: + return [] + + session = async_get_clientsession(hass) + headers = {"User-Agent": UA} + + out: list[dict[str, str]] = [] + seen: set[str] = set() + + def _add(ref: str | None, label: str, source: str) -> None: + r = (ref or "").strip() + if not r or r in seen: + return + seen.add(r) + out.append({"ref": r, "label": label, "source": source}) + + # Always offer default branch as an explicit option. + if default_branch and str(default_branch).strip(): + b = str(default_branch).strip() + _add(b, f"Branch: {b}", "branch") + + try: + if prov == "github": + # Releases + gh_headers = {"Accept": "application/vnd.github+json", "User-Agent": UA} + data, _ = await _safe_json( + session, + f"https://api.github.com/repos/{owner}/{repo}/releases?per_page={int(limit)}", + headers=gh_headers, + ) + if isinstance(data, list): + for r in data: + if not isinstance(r, dict): + continue + tag = r.get("tag_name") + name = r.get("name") + if tag: + lbl = str(tag) + if isinstance(name, str) and name.strip() and name.strip() != str(tag): + lbl = f"{tag} — {name.strip()}" + _add(str(tag), lbl, "release") + + # Tags + data, _ = await _safe_json( + session, + f"https://api.github.com/repos/{owner}/{repo}/tags?per_page={int(limit)}", + headers=gh_headers, + ) + if isinstance(data, list): + for t in data: + if isinstance(t, dict) and t.get("name"): + _add(str(t["name"]), str(t["name"]), "tag") + + return out + + if prov == "gitlab": + u = urlparse(repo_url.rstrip("/")) + base = f"{u.scheme}://{u.netloc}" + project = quote_plus(f"{owner}/{repo}") + + data, _ = await _safe_json( + session, + f"{base}/api/v4/projects/{project}/releases?per_page={int(limit)}", + headers=headers, + ) + if isinstance(data, list): + for r in data: + if not isinstance(r, dict): + continue + tag = r.get("tag_name") + name = r.get("name") + if tag: + lbl = str(tag) + if isinstance(name, str) and name.strip() and name.strip() != str(tag): + lbl = f"{tag} — {name.strip()}" + _add(str(tag), lbl, "release") + + data, _ = await _safe_json( + session, + f"{base}/api/v4/projects/{project}/repository/tags?per_page={int(limit)}", + headers=headers, + ) + if isinstance(data, list): + for t in data: + if isinstance(t, dict) and t.get("name"): + _add(str(t["name"]), str(t["name"]), "tag") + + return out + + # gitea (incl. Bahmcloud) + u = urlparse(repo_url.rstrip("/")) + base = f"{u.scheme}://{u.netloc}" + + data, _ = await _safe_json( + session, + f"{base}/api/v1/repos/{owner}/{repo}/releases?limit={int(limit)}", + headers=headers, + ) + if isinstance(data, list): + for r in data: + if not isinstance(r, dict): + continue + tag = r.get("tag_name") + name = r.get("name") + if tag: + lbl = str(tag) + if isinstance(name, str) and name.strip() and name.strip() != str(tag): + lbl = f"{tag} — {name.strip()}" + _add(str(tag), lbl, "release") + + data, _ = await _safe_json( + session, + f"{base}/api/v1/repos/{owner}/{repo}/tags?limit={int(limit)}", + headers=headers, + ) + if isinstance(data, list): + for t in data: + if isinstance(t, dict) and t.get("name"): + _add(str(t["name"]), str(t["name"]), "tag") + + return out + + except Exception: + _LOGGER.debug("fetch_repo_versions failed for %s", repo_url, exc_info=True) + return out \ No newline at end of file