Skip to content
Open
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
11 changes: 11 additions & 0 deletions src/OpenDeepWiki/Models/RepositoryDocResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,15 @@ public class RepositoryDocResponse
/// 记录生成此文档时读取的源代码文件路径
/// </summary>
public List<string> SourceFiles { get; set; } = [];

/// <summary>
/// Repository Git URL, used by the client to build source file links
/// for the correct hosting platform (GitHub, GitLab, Azure DevOps, ...).
/// </summary>
public string? GitUrl { get; set; }

/// <summary>
/// Branch the document was generated from, used as the ref in file links.
/// </summary>
public string? Branch { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,8 @@ public async Task<RepositoryDocResponse> GetDocAsync(string owner, string repo,
Slug = normalizedSlug,
Content = docFile.Content,
SourceFiles = sourceFiles,
GitUrl = repository.GitUrl,
Branch = branchEntity.BranchName,
Exists = true
};
}
Expand Down
7 changes: 4 additions & 3 deletions web/app/[owner]/[repo]/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,10 @@ export default async function RepoDocPage({ params, searchParams }: RepoDocPageP
dangerouslySetInnerHTML={{ __html: safeJsonLd(jsonLd) }}
/>
<MarkdownRenderer content={doc.content} language={locale} />
<SourceFiles
files={doc.sourceFiles || []}
branch={branch}
<SourceFiles
files={doc.sourceFiles || []}
gitUrl={doc.gitUrl ?? undefined}
branch={doc.branch ?? branch}
/>
</article>
{headings.length > 0 && (
Expand Down
61 changes: 40 additions & 21 deletions web/components/repo/source-files.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -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")) {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -97,8 +102,22 @@ export function SourceFiles({ files, gitUrl, branch }: SourceFilesProps) {
<div className="flex flex-wrap gap-2">
{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 (
<span
key={file}
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-sm bg-fd-secondary rounded-md text-fd-foreground"
title={file}
>
<FileCode2 className="h-3.5 w-3.5 text-fd-muted-foreground" />
<span className="max-w-[200px] truncate">{fileName}</span>
</span>
);
}

return (
<a
key={file}
Expand Down
2 changes: 2 additions & 0 deletions web/types/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface RepoDocResponse {
slug: string;
content: string;
sourceFiles: string[];
gitUrl: string | null;
branch: string | null;
}

export interface RepoHeading {
Expand Down