From b676f47ffa39121e057c5e1dc6c6e3750cfc4cd8 Mon Sep 17 00:00:00 2001 From: Jiri Sacha Date: Tue, 16 Jun 2026 16:11:11 +0200 Subject: [PATCH] Fix source file links for non-GitHub hosts (e.g. Azure DevOps) The "Sources" footer builds file links client-side in the SourceFiles component, but it diverged from the backend link builder in two ways: - The component was never given the repository git URL, so it fabricated a `https://github.com/{owner}/{repo}` URL from the route params. For repos not hosted on GitHub (Azure DevOps, self-hosted GitLab, ...) this produced links to a github.com repository that does not exist. - `buildFileUrl` had no Azure DevOps case, so even with a real git URL it fell back to the GitHub `/blob/` format, which is invalid for ADO. Meanwhile the in-content links are baked server-side by `WikiGenerator.BuildGitFileBaseUrl`, which already handles Azure DevOps, so those were correct - only the footer was wrong. Fix: - Return the repository `GitUrl` and `Branch` from the docs API (`RepositoryDocResponse` / `RepositoryDocsService.GetDocAsync`). - Thread `gitUrl` and `branch` into the `SourceFiles` component. - Add an Azure DevOps URL format to `buildFileUrl` (`?version=GB{branch}&path=/{file}`), mirroring the backend. - Drop the hardcoded github.com fallback: when the git URL is unknown the file is rendered as a plain label instead of a broken link. --- .../Models/RepositoryDocResponse.cs | 11 ++++ .../Repositories/RepositoryDocsService.cs | 2 + web/app/[owner]/[repo]/[...slug]/page.tsx | 7 ++- web/components/repo/source-files.tsx | 61 ++++++++++++------- web/types/repository.ts | 2 + 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/OpenDeepWiki/Models/RepositoryDocResponse.cs b/src/OpenDeepWiki/Models/RepositoryDocResponse.cs index ef9593dc..b352e06e 100644 --- a/src/OpenDeepWiki/Models/RepositoryDocResponse.cs +++ b/src/OpenDeepWiki/Models/RepositoryDocResponse.cs @@ -25,4 +25,15 @@ public class RepositoryDocResponse /// 记录生成此文档时读取的源代码文件路径 /// public List SourceFiles { get; set; } = []; + + /// + /// Repository Git URL, used by the client to build source file links + /// for the correct hosting platform (GitHub, GitLab, Azure DevOps, ...). + /// + public string? GitUrl { get; set; } + + /// + /// Branch the document was generated from, used as the ref in file links. + /// + public string? Branch { get; set; } } diff --git a/src/OpenDeepWiki/Services/Repositories/RepositoryDocsService.cs b/src/OpenDeepWiki/Services/Repositories/RepositoryDocsService.cs index 47e28d4f..2c861936 100644 --- a/src/OpenDeepWiki/Services/Repositories/RepositoryDocsService.cs +++ b/src/OpenDeepWiki/Services/Repositories/RepositoryDocsService.cs @@ -416,6 +416,8 @@ public async Task GetDocAsync(string owner, string repo, Slug = normalizedSlug, Content = docFile.Content, SourceFiles = sourceFiles, + GitUrl = repository.GitUrl, + Branch = branchEntity.BranchName, Exists = true }; } diff --git a/web/app/[owner]/[repo]/[...slug]/page.tsx b/web/app/[owner]/[repo]/[...slug]/page.tsx index 39830f86..df4d8130 100644 --- a/web/app/[owner]/[repo]/[...slug]/page.tsx +++ b/web/app/[owner]/[repo]/[...slug]/page.tsx @@ -176,9 +176,10 @@ export default async function RepoDocPage({ params, searchParams }: RepoDocPageP dangerouslySetInnerHTML={{ __html: safeJsonLd(jsonLd) }} /> - {headings.length > 0 && ( diff --git a/web/components/repo/source-files.tsx b/web/components/repo/source-files.tsx index 4a265f36..a8e4455e 100644 --- a/web/components/repo/source-files.tsx +++ b/web/components/repo/source-files.tsx @@ -1,9 +1,8 @@ "use client"; import { FileCode2, ExternalLink } from "lucide-react"; -import { useParams, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { useMemo } from "react"; -import { decodeRouteSegment } from "@/lib/repo-route"; interface SourceFilesProps { files: string[]; @@ -12,20 +11,26 @@ interface SourceFilesProps { } /** - * 构建文件的 Git 平台链接 + * Build a link to a file on the repository's Git hosting platform. + * Returns an empty string when the repository URL is unknown. */ function buildFileUrl(gitUrl: string, branch: string, filePath: string): string { - // 规范化 URL + // Without a known repository URL we cannot build a reliable link + if (!gitUrl) { + return ""; + } + + // Normalize the URL let normalizedUrl = gitUrl.replace(/\.git$/, "").trim(); - - // 转换 SSH 格式为 HTTPS + + // Convert SSH format to HTTPS if (normalizedUrl.startsWith("git@")) { normalizedUrl = normalizedUrl.replace("git@", "https://").replace(":", "/"); } - + normalizedUrl = normalizedUrl.replace(/\/$/, ""); - - // 根据平台构建 URL + + // Build the URL according to the hosting platform if (normalizedUrl.includes("github.com")) { return `${normalizedUrl}/blob/${branch}/${filePath}`; } else if (normalizedUrl.includes("gitlab.com") || normalizedUrl.includes("gitlab")) { @@ -34,23 +39,23 @@ function buildFileUrl(gitUrl: string, branch: string, filePath: string): string return `${normalizedUrl}/blob/${branch}/${filePath}`; } else if (normalizedUrl.includes("bitbucket.org")) { return `${normalizedUrl}/src/${branch}/${filePath}`; + } else if (normalizedUrl.includes("dev.azure.com") || normalizedUrl.includes("visualstudio.com")) { + // Azure DevOps: https://dev.azure.com/org/project/_git/repo?version=GBbranch&path=/path + return `${normalizedUrl}?version=GB${branch}&path=/${filePath}`; } - - // 默认使用 GitHub 格式 + + // Default: assume a GitHub-like format return `${normalizedUrl}/blob/${branch}/${filePath}`; } export function SourceFiles({ files, gitUrl, branch }: SourceFilesProps) { - const params = useParams(); const searchParams = useSearchParams(); - - // 从 URL 参数获取仓库信息 - const owner = decodeRouteSegment(params.owner as string); - const repo = decodeRouteSegment(params.repo as string); + + // Resolve the branch to link against const currentBranch = searchParams.get("branch") || branch || "main"; - - // 构建默认的 Git URL - const defaultGitUrl = gitUrl || `https://github.com/${owner}/${repo}`; + + // Only build links when the real repository URL is known + const repoGitUrl = gitUrl ?? ""; // 对文件进行分组(按目录) const groupedFiles = useMemo(() => { @@ -97,8 +102,22 @@ export function SourceFiles({ files, gitUrl, branch }: SourceFilesProps) {
{dirFiles.map(file => { const fileName = file.split("/").pop() || file; - const fileUrl = buildFileUrl(defaultGitUrl, currentBranch, file); - + const fileUrl = buildFileUrl(repoGitUrl, currentBranch, file); + + // Render a plain label when we cannot build a valid link + if (!fileUrl) { + return ( + + + {fileName} + + ); + } + return (