mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
Icon & link support for self-hosted repositories (#93)
This commit is contained in:
parent
01f4329d3e
commit
d6544086e7
12 changed files with 154 additions and 99 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Added
|
||||
|
||||
- Added file suggestions as a suggestion type. ([#88](https://github.com/sourcebot-dev/sourcebot/pull/88))
|
||||
- Added icon and link support for self-hosted repositories. ([#93](https://github.com/sourcebot-dev/sourcebot/pull/93))
|
||||
|
||||
## [2.5.0] - 2024-11-22
|
||||
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ interface RepositoryBadgeProps {
|
|||
const RepositoryBadge = ({
|
||||
repo
|
||||
}: RepositoryBadgeProps) => {
|
||||
const { repoIcon, repoName, repoLink } = (() => {
|
||||
const info = getRepoCodeHostInfo(repo.Name);
|
||||
const { repoIcon, displayName, repoLink } = (() => {
|
||||
const info = getRepoCodeHostInfo(repo);
|
||||
|
||||
if (info) {
|
||||
return {
|
||||
|
|
@ -66,14 +66,14 @@ const RepositoryBadge = ({
|
|||
alt={info.costHostName}
|
||||
className={`w-4 h-4 ${info.iconClassName}`}
|
||||
/>,
|
||||
repoName: info.repoName,
|
||||
displayName: info.displayName,
|
||||
repoLink: info.repoLink,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
repoIcon: <FileIcon className="w-4 h-4" />,
|
||||
repoName: repo.Name,
|
||||
displayName: repo.Name,
|
||||
repoLink: undefined,
|
||||
}
|
||||
})();
|
||||
|
|
@ -91,7 +91,7 @@ const RepositoryBadge = ({
|
|||
>
|
||||
{repoIcon}
|
||||
<span className="text-sm font-mono">
|
||||
{repoName}
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||
import { Column, ColumnDef } from "@tanstack/react-table"
|
||||
import { ArrowUpDown } from "lucide-react"
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
|
@ -19,6 +18,7 @@ export type RepositoryColumnInfo = {
|
|||
lastIndexed: string;
|
||||
latestCommit: string;
|
||||
commitUrlTemplate: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
||||
|
|
@ -27,14 +27,16 @@ export const columns: ColumnDef<RepositoryColumnInfo>[] = [
|
|||
header: "Name",
|
||||
cell: ({ row }) => {
|
||||
const repo = row.original;
|
||||
const info = getRepoCodeHostInfo(repo.name);
|
||||
const url = repo.url;
|
||||
// local repositories will have a url of 0 length
|
||||
const isRemoteRepo = url.length === 0;
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span
|
||||
className={info?.repoLink ? "cursor-pointer text-blue-500 hover:underline": ""}
|
||||
className={!isRemoteRepo ? "cursor-pointer text-blue-500 hover:underline": ""}
|
||||
onClick={() => {
|
||||
if (info?.repoLink) {
|
||||
window.open(info.repoLink, "_blank");
|
||||
if (!isRemoteRepo) {
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const RepositoryTable = async () => {
|
|||
latestCommit: repo.Repository.LatestCommitDate,
|
||||
indexedFiles: repo.Stats.Documents,
|
||||
commitUrlTemplate: repo.Repository.CommitURLTemplate,
|
||||
url: repo.Repository.URL,
|
||||
}
|
||||
}).sort((a, b) => {
|
||||
return new Date(b.lastIndexed).getTime() - new Date(a.lastIndexed).getTime();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { fetchFileSource } from "@/app/api/(client)/client";
|
||||
import { getCodeHostFilePreviewLink, base64Decode } from "@/lib/utils";
|
||||
import { base64Decode } from "@/lib/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CodePreview, CodePreviewFile } from "./codePreview";
|
||||
import { SearchResultFile } from "@/lib/types";
|
||||
|
|
@ -11,6 +11,7 @@ interface CodePreviewPanelProps {
|
|||
onClose: () => void;
|
||||
selectedMatchIndex: number;
|
||||
onSelectedMatchIndexChange: (index: number) => void;
|
||||
repoUrlTemplates: Record<string, string>;
|
||||
}
|
||||
|
||||
export const CodePreviewPanel = ({
|
||||
|
|
@ -18,6 +19,7 @@ export const CodePreviewPanel = ({
|
|||
onClose,
|
||||
selectedMatchIndex,
|
||||
onSelectedMatchIndexChange,
|
||||
repoUrlTemplates,
|
||||
}: CodePreviewPanelProps) => {
|
||||
|
||||
const { data: file } = useQuery({
|
||||
|
|
@ -37,8 +39,15 @@ export const CodePreviewPanel = ({
|
|||
branch,
|
||||
})
|
||||
.then(({ source }) => {
|
||||
// @todo : refector this to use the templates provided by zoekt.
|
||||
const link = getCodeHostFilePreviewLink(fileMatch.Repository, fileMatch.FileName, branch);
|
||||
const link = (() => {
|
||||
const template = repoUrlTemplates[fileMatch.Repository];
|
||||
if (!template) {
|
||||
return undefined;
|
||||
}
|
||||
return template
|
||||
.replace("{{.Version}}", branch ?? "HEAD")
|
||||
.replace("{{.Path}}", fileMatch.FileName);
|
||||
})();
|
||||
|
||||
const decodedSource = base64Decode(source);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,13 @@
|
|||
|
||||
import { QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
||||
import clsx from "clsx";
|
||||
import Image from "next/image";
|
||||
|
||||
export type Entry = {
|
||||
key: string;
|
||||
displayName: string;
|
||||
count: number;
|
||||
isSelected: boolean;
|
||||
icon?: string;
|
||||
iconAltText?: string;
|
||||
iconClassName?: string;
|
||||
Icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface EntryProps {
|
||||
|
|
@ -22,11 +19,9 @@ interface EntryProps {
|
|||
export const Entry = ({
|
||||
entry: {
|
||||
isSelected,
|
||||
icon,
|
||||
iconAltText,
|
||||
iconClassName,
|
||||
displayName,
|
||||
count,
|
||||
Icon,
|
||||
},
|
||||
onClicked,
|
||||
}: EntryProps) => {
|
||||
|
|
@ -42,13 +37,7 @@ export const Entry = ({
|
|||
onClick={() => onClicked()}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{icon ? (
|
||||
<Image
|
||||
src={icon}
|
||||
alt={iconAltText ?? ''}
|
||||
className={`w-4 h-4 flex-shrink-0 ${iconClassName}`}
|
||||
/>
|
||||
) : (
|
||||
{Icon ? Icon : (
|
||||
<QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" />
|
||||
)}
|
||||
<p className="text-wrap">{displayName}</p>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
'use client';
|
||||
|
||||
import { SearchResultFile } from "@/lib/types";
|
||||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||
import { Repository, SearchResultFile } from "@/lib/types";
|
||||
import { cn, getRepoCodeHostInfo } from "@/lib/utils";
|
||||
import { SetStateAction, useCallback, useEffect, useState } from "react";
|
||||
import { Entry } from "./entry";
|
||||
import { Filter } from "./filter";
|
||||
import { getLanguageIcon } from "./languageIcons";
|
||||
import Image from "next/image";
|
||||
import { LaptopIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
||||
|
||||
interface FilePanelProps {
|
||||
matches: SearchResultFile[];
|
||||
onFilterChanged: (filteredMatches: SearchResultFile[]) => void,
|
||||
repoMetadata: Record<string, Repository>;
|
||||
}
|
||||
|
||||
export const FilterPanel = ({
|
||||
matches,
|
||||
onFilterChanged,
|
||||
repoMetadata,
|
||||
}: FilePanelProps) => {
|
||||
const [repos, setRepos] = useState<Record<string, Entry>>({});
|
||||
const [languages, setLanguages] = useState<Record<string, Entry>>({});
|
||||
|
|
@ -24,15 +28,24 @@ export const FilterPanel = ({
|
|||
"Repository",
|
||||
matches,
|
||||
(key) => {
|
||||
const info = getRepoCodeHostInfo(key);
|
||||
const repo: Repository | undefined = repoMetadata[key];
|
||||
const info = getRepoCodeHostInfo(repo);
|
||||
const Icon = info ? (
|
||||
<Image
|
||||
src={info.icon}
|
||||
alt={info.costHostName}
|
||||
className={cn('w-4 h-4 flex-shrink-0', info.iconClassName)}
|
||||
/>
|
||||
) : (
|
||||
<LaptopIcon className="w-4 h-4 flex-shrink-0" />
|
||||
);
|
||||
|
||||
return {
|
||||
key,
|
||||
displayName: info?.repoName ?? key,
|
||||
displayName: info?.displayName ?? key,
|
||||
count: 0,
|
||||
isSelected: false,
|
||||
icon: info?.icon,
|
||||
iconAltText: info?.costHostName,
|
||||
iconClassName: info?.iconClassName,
|
||||
Icon,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
@ -45,12 +58,23 @@ export const FilterPanel = ({
|
|||
"Language",
|
||||
matches,
|
||||
(key) => {
|
||||
const iconSrc = getLanguageIcon(key);
|
||||
const Icon = iconSrc ? (
|
||||
<Image
|
||||
src={iconSrc}
|
||||
alt={key}
|
||||
className="w-4 h-4 flex-shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<QuestionMarkCircledIcon className="w-4 h-4 flex-shrink-0" />
|
||||
);
|
||||
|
||||
return {
|
||||
key,
|
||||
displayName: key,
|
||||
count: 0,
|
||||
isSelected: false,
|
||||
icon: getLanguageIcon(key),
|
||||
Icon: Icon,
|
||||
} satisfies Entry;
|
||||
}
|
||||
)
|
||||
|
|
@ -85,10 +109,10 @@ export const FilterPanel = ({
|
|||
);
|
||||
|
||||
const filteredMatches = matches.filter((match) =>
|
||||
(
|
||||
(selectedRepos.size === 0 ? true : selectedRepos.has(match.Repository)) &&
|
||||
(selectedLanguages.size === 0 ? true : selectedLanguages.has(match.Language))
|
||||
)
|
||||
(
|
||||
(selectedRepos.size === 0 ? true : selectedRepos.has(match.Repository)) &&
|
||||
(selectedLanguages.size === 0 ? true : selectedLanguages.has(match.Language))
|
||||
)
|
||||
);
|
||||
|
||||
onFilterChanged(filteredMatches);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
import { getRepoCodeHostInfo } from "@/lib/utils";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import Image from "next/image";
|
||||
import { DoubleArrowDownIcon, DoubleArrowUpIcon, FileIcon } from "@radix-ui/react-icons";
|
||||
import { DoubleArrowDownIcon, DoubleArrowUpIcon, LaptopIcon } from "@radix-ui/react-icons";
|
||||
import clsx from "clsx";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SearchResultFile } from "@/lib/types";
|
||||
import { Repository, SearchResultFile } from "@/lib/types";
|
||||
import { FileMatch } from "./fileMatch";
|
||||
|
||||
export const MAX_MATCHES_TO_PREVIEW = 3;
|
||||
|
|
@ -18,6 +18,7 @@ interface FileMatchContainerProps {
|
|||
showAllMatches: boolean;
|
||||
onShowAllMatchesButtonClicked: () => void;
|
||||
isBranchFilteringEnabled: boolean;
|
||||
repoMetadata: Record<string, Repository>;
|
||||
}
|
||||
|
||||
export const FileMatchContainer = ({
|
||||
|
|
@ -27,6 +28,7 @@ export const FileMatchContainer = ({
|
|||
showAllMatches,
|
||||
onShowAllMatchesButtonClicked,
|
||||
isBranchFilteringEnabled,
|
||||
repoMetadata,
|
||||
}: FileMatchContainerProps) => {
|
||||
|
||||
const matchCount = useMemo(() => {
|
||||
|
|
@ -59,11 +61,13 @@ export const FileMatchContainer = ({
|
|||
return null;
|
||||
}, [matches]);
|
||||
|
||||
const { repoIcon, repoName, repoLink } = useMemo(() => {
|
||||
const info = getRepoCodeHostInfo(file.Repository);
|
||||
const { repoIcon, displayName, repoLink } = useMemo(() => {
|
||||
const repo: Repository | undefined = repoMetadata[file.Repository];
|
||||
const info = getRepoCodeHostInfo(repo);
|
||||
|
||||
if (info) {
|
||||
return {
|
||||
repoName: info.repoName,
|
||||
displayName: info.displayName,
|
||||
repoLink: info.repoLink,
|
||||
repoIcon: <Image
|
||||
src={info.icon}
|
||||
|
|
@ -74,11 +78,11 @@ export const FileMatchContainer = ({
|
|||
}
|
||||
|
||||
return {
|
||||
repoName: file.Repository,
|
||||
displayName: file.Repository,
|
||||
repoLink: undefined,
|
||||
repoIcon: <FileIcon className="w-4 h-4" />
|
||||
repoIcon: <LaptopIcon className="w-4 h-4" />
|
||||
}
|
||||
}, [file]);
|
||||
}, [file.Repository, repoMetadata]);
|
||||
|
||||
const isMoreContentButtonVisible = useMemo(() => {
|
||||
return matchCount > MAX_MATCHES_TO_PREVIEW;
|
||||
|
|
@ -122,7 +126,7 @@ export const FileMatchContainer = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
{repoName}
|
||||
{displayName}
|
||||
</span>
|
||||
{isBranchFilteringEnabled && branches.length > 0 && (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { SearchResultFile } from "@/lib/types";
|
||||
import { Repository, SearchResultFile } from "@/lib/types";
|
||||
import { FileMatchContainer, MAX_MATCHES_TO_PREVIEW } from "./fileMatchContainer";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
|
|
@ -12,6 +12,7 @@ interface SearchResultsPanelProps {
|
|||
isLoadMoreButtonVisible: boolean;
|
||||
onLoadMoreButtonClicked: () => void;
|
||||
isBranchFilteringEnabled: boolean;
|
||||
repoMetadata: Record<string, Repository>;
|
||||
}
|
||||
|
||||
const ESTIMATED_LINE_HEIGHT_PX = 20;
|
||||
|
|
@ -25,6 +26,7 @@ export const SearchResultsPanel = ({
|
|||
isLoadMoreButtonVisible,
|
||||
onLoadMoreButtonClicked,
|
||||
isBranchFilteringEnabled,
|
||||
repoMetadata,
|
||||
}: SearchResultsPanelProps) => {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const [showAllMatchesStates, setShowAllMatchesStates] = useState(Array(fileMatches.length).fill(false));
|
||||
|
|
@ -148,6 +150,7 @@ export const SearchResultsPanel = ({
|
|||
onShowAllMatchesButtonClicked(virtualRow.index);
|
||||
}}
|
||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoMetadata={repoMetadata}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||
import { SearchQueryParams, SearchResultFile } from "@/lib/types";
|
||||
import { Repository, SearchQueryParams, SearchResultFile } from "@/lib/types";
|
||||
import { createPathWithQueryParams } from "@/lib/utils";
|
||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -17,7 +17,7 @@ import { useRouter } from "next/navigation";
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import logoDark from "../../../public/sb_logo_dark.png";
|
||||
import logoLight from "../../../public/sb_logo_light.png";
|
||||
import { search } from "../api/(client)/client";
|
||||
import { getRepos, search } from "../api/(client)/client";
|
||||
import { SearchBar } from "../components/searchBar";
|
||||
import { SettingsDropdown } from "../components/settingsDropdown";
|
||||
import { CodePreviewPanel } from "./components/codePreviewPanel";
|
||||
|
|
@ -45,6 +45,26 @@ export default function SearchPage() {
|
|||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
// Use the /api/repos endpoint to get a useful list of
|
||||
// repository metadata (like host type, repo name, etc.)
|
||||
// Convert this into a map of repo name to repo metadata
|
||||
// for easy lookup.
|
||||
const { data: repoMetadata } = useQuery({
|
||||
queryKey: ["repos"],
|
||||
queryFn: () => getRepos(),
|
||||
select: (data): Record<string, Repository> =>
|
||||
data.List.Repos
|
||||
.map(r => r.Repository)
|
||||
.reduce(
|
||||
(acc, repo) => ({
|
||||
...acc,
|
||||
[repo.Name]: repo,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchResponse) {
|
||||
return;
|
||||
|
|
@ -77,13 +97,14 @@ export default function SearchPage() {
|
|||
});
|
||||
}, [captureEvent, searchResponse]);
|
||||
|
||||
const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled } = useMemo(() => {
|
||||
const { fileMatches, searchDurationMs, totalMatchCount, isBranchFilteringEnabled, repoUrlTemplates } = useMemo(() => {
|
||||
if (!searchResponse) {
|
||||
return {
|
||||
fileMatches: [],
|
||||
searchDurationMs: 0,
|
||||
totalMatchCount: 0,
|
||||
isBranchFilteringEnabled: false,
|
||||
repoUrlTemplates: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +129,7 @@ export default function SearchPage() {
|
|||
searchDurationMs: Math.round(searchResponse.Result.Duration / 1000000),
|
||||
totalMatchCount: searchResponse.Result.MatchCount,
|
||||
isBranchFilteringEnabled,
|
||||
repoUrlTemplates: searchResponse.Result.RepoURLs,
|
||||
}
|
||||
}, [searchResponse]);
|
||||
|
||||
|
|
@ -202,6 +224,8 @@ export default function SearchPage() {
|
|||
isMoreResultsButtonVisible={isMoreResultsButtonVisible}
|
||||
onLoadMoreResults={onLoadMoreResults}
|
||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoUrlTemplates={repoUrlTemplates}
|
||||
repoMetadata={repoMetadata ?? {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -213,6 +237,8 @@ interface PanelGroupProps {
|
|||
isMoreResultsButtonVisible?: boolean;
|
||||
onLoadMoreResults: () => void;
|
||||
isBranchFilteringEnabled: boolean;
|
||||
repoUrlTemplates: Record<string, string>;
|
||||
repoMetadata: Record<string, Repository>;
|
||||
}
|
||||
|
||||
const PanelGroup = ({
|
||||
|
|
@ -220,6 +246,8 @@ const PanelGroup = ({
|
|||
isMoreResultsButtonVisible,
|
||||
onLoadMoreResults,
|
||||
isBranchFilteringEnabled,
|
||||
repoUrlTemplates,
|
||||
repoMetadata,
|
||||
}: PanelGroupProps) => {
|
||||
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
||||
const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined);
|
||||
|
|
@ -254,6 +282,7 @@ const PanelGroup = ({
|
|||
<FilterPanel
|
||||
matches={fileMatches}
|
||||
onFilterChanged={onFilterChanged}
|
||||
repoMetadata={repoMetadata}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
|
|
@ -278,6 +307,7 @@ const PanelGroup = ({
|
|||
isLoadMoreButtonVisible={!!isMoreResultsButtonVisible}
|
||||
onLoadMoreButtonClicked={onLoadMoreResults}
|
||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoMetadata={repoMetadata}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
|
|
@ -302,6 +332,7 @@ const PanelGroup = ({
|
|||
onClose={() => setSelectedFile(undefined)}
|
||||
selectedMatchIndex={selectedMatchIndex}
|
||||
onSelectedMatchIndexChange={setSelectedMatchIndex}
|
||||
repoUrlTemplates={repoUrlTemplates}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export const zoektSearchResponseSchema = z.object({
|
|||
// Set if `whole` is true.
|
||||
Content: z.string().optional(),
|
||||
})).nullable(),
|
||||
RepoURLs: z.record(z.string(), z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import githubLogo from "../../public/github.svg";
|
|||
import gitlabLogo from "../../public/gitlab.svg";
|
||||
import giteaLogo from "../../public/gitea.svg";
|
||||
import { ServiceError } from "./serviceError";
|
||||
import { Repository } from "./types";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
|
|
@ -31,64 +32,53 @@ export const createPathWithQueryParams = (path: string, ...queryParams: [string,
|
|||
|
||||
type CodeHostInfo = {
|
||||
type: "github" | "gitlab" | "gitea";
|
||||
repoName: string;
|
||||
displayName: string;
|
||||
costHostName: string;
|
||||
repoLink: string;
|
||||
icon: string;
|
||||
iconClassName?: string;
|
||||
}
|
||||
|
||||
export const getRepoCodeHostInfo = (repoName: string): CodeHostInfo | undefined => {
|
||||
if (repoName.startsWith("github.com")) {
|
||||
return {
|
||||
type: "github",
|
||||
repoName: repoName.substring("github.com/".length),
|
||||
costHostName: "GitHub",
|
||||
repoLink: `https://${repoName}`,
|
||||
icon: githubLogo,
|
||||
iconClassName: "dark:invert",
|
||||
}
|
||||
export const getRepoCodeHostInfo = (repo?: Repository): CodeHostInfo | undefined => {
|
||||
if (!repo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (repoName.startsWith("gitlab.com")) {
|
||||
return {
|
||||
type: "gitlab",
|
||||
repoName: repoName.substring("gitlab.com/".length),
|
||||
costHostName: "GitLab",
|
||||
repoLink: `https://${repoName}`,
|
||||
icon: gitlabLogo,
|
||||
}
|
||||
const hostType = repo.RawConfig ? repo.RawConfig['web-url-type'] : undefined;
|
||||
if (!hostType) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (repoName.startsWith("gitea.com")) {
|
||||
return {
|
||||
type: "gitea",
|
||||
repoName: repoName.substring("gitea.com/".length),
|
||||
costHostName: "Gitea",
|
||||
repoLink: `https://${repoName}`,
|
||||
icon: giteaLogo,
|
||||
}
|
||||
const url = new URL(repo.URL);
|
||||
const displayName = url.pathname.slice(1);
|
||||
|
||||
switch (hostType) {
|
||||
case 'github':
|
||||
return {
|
||||
type: "github",
|
||||
displayName: displayName,
|
||||
costHostName: "GitHub",
|
||||
repoLink: repo.URL,
|
||||
icon: githubLogo,
|
||||
iconClassName: "dark:invert",
|
||||
}
|
||||
case 'gitlab':
|
||||
return {
|
||||
type: "gitlab",
|
||||
displayName: displayName,
|
||||
costHostName: "GitLab",
|
||||
repoLink: repo.URL,
|
||||
icon: gitlabLogo,
|
||||
}
|
||||
case 'gitea':
|
||||
return {
|
||||
type: "gitea",
|
||||
displayName: displayName,
|
||||
costHostName: "Gitea",
|
||||
repoLink: repo.URL,
|
||||
icon: giteaLogo,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const getCodeHostFilePreviewLink = (repoName: string, filePath: string, branch: string = "HEAD"): string | undefined => {
|
||||
const info = getRepoCodeHostInfo(repoName);
|
||||
|
||||
if (info?.type === "github") {
|
||||
return `${info.repoLink}/blob/${branch}/${filePath}`;
|
||||
}
|
||||
|
||||
if (info?.type === "gitlab") {
|
||||
return `${info.repoLink}/-/blob/${branch}/${filePath}`;
|
||||
}
|
||||
|
||||
if (info?.type === "gitea") {
|
||||
return `${info.repoLink}/src/branch/${branch}/${filePath}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const isServiceError = (data: unknown): data is ServiceError => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue