mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
Store HEAD commit hash in DB. Also improve how we display dates
This commit is contained in:
parent
cd54eb0b7b
commit
8168ec2c1f
12 changed files with 184 additions and 54 deletions
|
|
@ -269,3 +269,15 @@ export const getTags = async (path: string) => {
|
||||||
const tags = await git.tags();
|
const tags = await git.tags();
|
||||||
return tags.all;
|
return tags.all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getCommitHashForRefName = async ({
|
||||||
|
path,
|
||||||
|
refName,
|
||||||
|
}: {
|
||||||
|
path: string,
|
||||||
|
refName: string,
|
||||||
|
}) => {
|
||||||
|
const git = createGitClientForPath(path);
|
||||||
|
const rev = await git.revparse(refName);
|
||||||
|
return rev;
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import { Job, Queue, ReservedJob, Worker } from "groupmq";
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { INDEX_CACHE_DIR } from './constants.js';
|
import { INDEX_CACHE_DIR } from './constants.js';
|
||||||
import { env } from './env.js';
|
import { env } from './env.js';
|
||||||
import { cloneRepository, fetchRepository, isPathAValidGitRepoRoot, unsetGitConfig, upsertGitConfig } from './git.js';
|
import { cloneRepository, fetchRepository, getCommitHashForRefName, isPathAValidGitRepoRoot, unsetGitConfig, upsertGitConfig } from './git.js';
|
||||||
import { PromClient } from './promClient.js';
|
import { PromClient } from './promClient.js';
|
||||||
import { repoMetadataSchema, RepoWithConnections, Settings } from "./types.js";
|
import { repoMetadataSchema, RepoWithConnections, Settings } from "./types.js";
|
||||||
import { getAuthCredentialsForRepo, getRepoPath, getShardPrefix, groupmqLifecycleExceptionWrapper, measure } from './utils.js';
|
import { getAuthCredentialsForRepo, getRepoPath, getShardPrefix, groupmqLifecycleExceptionWrapper, measure } from './utils.js';
|
||||||
|
|
@ -384,16 +384,26 @@ export class RepoIndexManager {
|
||||||
data: {
|
data: {
|
||||||
status: RepoIndexingJobStatus.COMPLETED,
|
status: RepoIndexingJobStatus.COMPLETED,
|
||||||
completedAt: new Date(),
|
completedAt: new Date(),
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
repo: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const jobTypeLabel = getJobTypePrometheusLabel(jobData.type);
|
const jobTypeLabel = getJobTypePrometheusLabel(jobData.type);
|
||||||
|
|
||||||
if (jobData.type === RepoIndexingJobType.INDEX) {
|
if (jobData.type === RepoIndexingJobType.INDEX) {
|
||||||
|
const { path: repoPath } = getRepoPath(jobData.repo);
|
||||||
|
const commitHash = await getCommitHashForRefName({
|
||||||
|
path: repoPath,
|
||||||
|
refName: 'HEAD',
|
||||||
|
});
|
||||||
|
|
||||||
const repo = await this.db.repo.update({
|
const repo = await this.db.repo.update({
|
||||||
where: { id: jobData.repoId },
|
where: { id: jobData.repoId },
|
||||||
data: {
|
data: {
|
||||||
indexedAt: new Date(),
|
indexedAt: new Date(),
|
||||||
|
indexedCommitHash: commitHash,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Repo" ADD COLUMN "indexedCommitHash" TEXT;
|
||||||
|
|
@ -50,6 +50,7 @@ model Repo {
|
||||||
|
|
||||||
jobs RepoIndexingJob[]
|
jobs RepoIndexingJob[]
|
||||||
indexedAt DateTime? /// When the repo was last indexed successfully.
|
indexedAt DateTime? /// When the repo was last indexed successfully.
|
||||||
|
indexedCommitHash String? /// The commit hash of the last indexed commit (on HEAD).
|
||||||
|
|
||||||
external_id String /// The id of the repo in the external service
|
external_id String /// The id of the repo in the external service
|
||||||
external_codeHostType String /// The type of the external service (e.g., github, gitlab, etc.)
|
external_codeHostType String /// The type of the external service (e.g., github, gitlab, etc.)
|
||||||
|
|
|
||||||
36
packages/web/src/app/[domain]/components/DisplayDate.tsx
Normal file
36
packages/web/src/app/[domain]/components/DisplayDate.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { getFormattedDate } from "@/lib/utils"
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
|
|
||||||
|
const formatFullDate = (date: Date) => {
|
||||||
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZoneName: "short",
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisplayDateProps {
|
||||||
|
date: Date
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayDate = ({ date, className }: DisplayDateProps) => {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className={className}>
|
||||||
|
{getFormattedDate(date)}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{formatFullDate(date)}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
VscSymbolVariable
|
VscSymbolVariable
|
||||||
} from "react-icons/vsc";
|
} from "react-icons/vsc";
|
||||||
import { useSearchHistory } from "@/hooks/useSearchHistory";
|
import { useSearchHistory } from "@/hooks/useSearchHistory";
|
||||||
import { getDisplayTime, isServiceError, unwrapServiceError } from "@/lib/utils";
|
import { getFormattedDate, isServiceError, unwrapServiceError } from "@/lib/utils";
|
||||||
import { useDomain } from "@/hooks/useDomain";
|
import { useDomain } from "@/hooks/useDomain";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ export const useSuggestionsData = ({
|
||||||
const searchHistorySuggestions = useMemo(() => {
|
const searchHistorySuggestions = useMemo(() => {
|
||||||
return searchHistory.map(search => ({
|
return searchHistory.map(search => ({
|
||||||
value: search.query,
|
value: search.query,
|
||||||
description: getDisplayTime(new Date(search.date)),
|
description: getFormattedDate(new Date(search.date)),
|
||||||
} satisfies Suggestion));
|
} satisfies Suggestion));
|
||||||
}, [searchHistory]);
|
}, [searchHistory]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,7 @@ import { Suspense } from "react"
|
||||||
import { RepoJobsTable } from "../components/repoJobsTable"
|
import { RepoJobsTable } from "../components/repoJobsTable"
|
||||||
import { getConfigSettings } from "@sourcebot/shared"
|
import { getConfigSettings } from "@sourcebot/shared"
|
||||||
import { env } from "@/env.mjs"
|
import { env } from "@/env.mjs"
|
||||||
|
import { DisplayDate } from "../../components/DisplayDate"
|
||||||
function formatDate(date: Date | null) {
|
|
||||||
if (!date) return "Never"
|
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
}).format(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function RepoDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default async function RepoDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
|
|
@ -109,7 +99,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-semibold">{formatDate(repo.createdAt)}</div>
|
<DisplayDate date={repo.createdAt} className="text-2xl font-semibold"/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -128,7 +118,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-semibold">{repo.indexedAt ? formatDate(repo.indexedAt) : "Never"}</div>
|
{repo.indexedAt ? <DisplayDate date={repo.indexedAt} className="text-2xl font-semibold"/> : "Never" }
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -147,7 +137,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-semibold">{nextIndexAttempt ? formatDate(nextIndexAttempt) : "-"}</div>
|
{nextIndexAttempt ? <DisplayDate date={nextIndexAttempt} className="text-2xl font-semibold"/> : "-" }
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { useMemo } from "react"
|
||||||
import { LightweightCodeHighlighter } from "../../components/lightweightCodeHighlighter"
|
import { LightweightCodeHighlighter } from "../../components/lightweightCodeHighlighter"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useToast } from "@/components/hooks/use-toast"
|
import { useToast } from "@/components/hooks/use-toast"
|
||||||
|
import { DisplayDate } from "../../components/DisplayDate"
|
||||||
|
|
||||||
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
||||||
|
|
||||||
|
|
@ -68,17 +69,6 @@ const getTypeBadge = (type: RepoIndexingJob["type"]) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date: Date | null) => {
|
|
||||||
if (!date) return "-"
|
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
}).format(date)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDuration = (start: Date, end: Date | null) => {
|
const getDuration = (start: Date, end: Date | null) => {
|
||||||
if (!end) return "-"
|
if (!end) return "-"
|
||||||
const diff = end.getTime() - start.getTime()
|
const diff = end.getTime() - start.getTime()
|
||||||
|
|
@ -139,7 +129,7 @@ export const columns: ColumnDef<RepoIndexingJob>[] = [
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cell: ({ row }) => formatDate(row.getValue("createdAt")),
|
cell: ({ row }) => <DisplayDate date={row.getValue("createdAt") as Date} className="ml-3"/>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "completedAt",
|
accessorKey: "completedAt",
|
||||||
|
|
@ -151,7 +141,14 @@ export const columns: ColumnDef<RepoIndexingJob>[] = [
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cell: ({ row }) => formatDate(row.getValue("completedAt")),
|
cell: ({ row }) => {
|
||||||
|
const completedAt = row.getValue("completedAt") as Date | null;
|
||||||
|
if (!completedAt) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DisplayDate date={completedAt} className="ml-3"/>
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "duration",
|
id: "duration",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
|
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
|
||||||
import { getCodeHostInfoForRepo, getRepoImageSrc } from "@/lib/utils"
|
import { CodeHostType, getCodeHostCommitUrl, getCodeHostInfoForRepo, getFormattedDate, getRepoImageSrc } from "@/lib/utils"
|
||||||
import {
|
import {
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
type ColumnFiltersState,
|
type ColumnFiltersState,
|
||||||
|
|
@ -35,6 +35,7 @@ import { useMemo, useState } from "react"
|
||||||
import { getBrowsePath } from "../../browse/hooks/utils"
|
import { getBrowsePath } from "../../browse/hooks/utils"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useToast } from "@/components/hooks/use-toast";
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
|
import { DisplayDate } from "../../components/DisplayDate"
|
||||||
|
|
||||||
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
||||||
|
|
||||||
|
|
@ -49,6 +50,7 @@ export type Repo = {
|
||||||
webUrl: string | null
|
webUrl: string | null
|
||||||
codeHostType: string
|
codeHostType: string
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
|
indexedCommitHash: string | null
|
||||||
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,13 +83,7 @@ const getStatusBadge = (status: Repo["latestJobStatus"]) => {
|
||||||
|
|
||||||
const formatDate = (date: Date | null) => {
|
const formatDate = (date: Date | null) => {
|
||||||
if (!date) return "Never"
|
if (!date) return "Never"
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
return getFormattedDate(date);
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
}).format(date)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const columns: ColumnDef<Repo>[] = [
|
export const columns: ColumnDef<Repo>[] = [
|
||||||
|
|
@ -132,20 +128,64 @@ export const columns: ColumnDef<Repo>[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "latestJobStatus",
|
accessorKey: "latestJobStatus",
|
||||||
header: "Status",
|
header: "Lastest status",
|
||||||
cell: ({ row }) => getStatusBadge(row.getValue("latestJobStatus")),
|
cell: ({ row }) => getStatusBadge(row.getValue("latestJobStatus")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "indexedAt",
|
accessorKey: "indexedAt",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
<Button
|
||||||
Last Indexed
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Last synced
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cell: ({ row }) => formatDate(row.getValue("indexedAt")),
|
cell: ({ row }) => {
|
||||||
|
const indexedAt = row.getValue("indexedAt") as Date | null;
|
||||||
|
if (!indexedAt) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DisplayDate date={indexedAt} className="ml-3"/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "indexedCommitHash",
|
||||||
|
header: "Last commit",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const hash = row.getValue("indexedCommitHash") as string | null;
|
||||||
|
if (!hash) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const smallHash = hash.slice(0, 7);
|
||||||
|
const repo = row.original;
|
||||||
|
const codeHostType = repo.codeHostType as CodeHostType;
|
||||||
|
const webUrl = repo.webUrl;
|
||||||
|
|
||||||
|
const commitUrl = getCodeHostCommitUrl({
|
||||||
|
webUrl,
|
||||||
|
codeHostType,
|
||||||
|
commitHash: hash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!commitUrl) {
|
||||||
|
return <span className="font-mono text-sm">{smallHash}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Link
|
||||||
|
href={commitUrl}
|
||||||
|
className="font-mono text-sm text-link hover:underline"
|
||||||
|
>
|
||||||
|
{smallHash}
|
||||||
|
</Link>
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export default async function ReposPage() {
|
||||||
imageUrl: repo.imageUrl,
|
imageUrl: repo.imageUrl,
|
||||||
latestJobStatus: repo.jobs.length > 0 ? repo.jobs[0].status : null,
|
latestJobStatus: repo.jobs.length > 0 ? repo.jobs[0].status : null,
|
||||||
codeHostType: repo.external_codeHostType,
|
codeHostType: repo.external_codeHostType,
|
||||||
|
indexedCommitHash: repo.indexedCommitHash,
|
||||||
}))} />
|
}))} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -44,6 +45,9 @@ const getReposWithLatestJob = async () => sew(() =>
|
||||||
},
|
},
|
||||||
take: 1
|
take: 1
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
name: 'asc'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return repos;
|
return repos;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
|
||||||
import { LucideKeyRound, MoreVertical, Search, LucideTrash } from "lucide-react";
|
import { LucideKeyRound, MoreVertical, Search, LucideTrash } from "lucide-react";
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { getDisplayTime, isServiceError } from "@/lib/utils";
|
import { getFormattedDate, isServiceError } from "@/lib/utils";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||||
|
|
@ -104,7 +104,7 @@ export const SecretsList = ({ secrets }: SecretsListProps) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Created {getDisplayTime(secret.createdAt)}
|
Created {getFormattedDate(secret.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,38 @@ export const getCodeHostIcon = (codeHostType: string): { src: string, className?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getCodeHostCommitUrl = ({
|
||||||
|
webUrl,
|
||||||
|
codeHostType,
|
||||||
|
commitHash,
|
||||||
|
}: {
|
||||||
|
webUrl?: string | null,
|
||||||
|
codeHostType: CodeHostType,
|
||||||
|
commitHash: string,
|
||||||
|
}) => {
|
||||||
|
if (!webUrl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (codeHostType) {
|
||||||
|
case 'github':
|
||||||
|
return `${webUrl}/commit/${commitHash}`;
|
||||||
|
case 'gitlab':
|
||||||
|
return `${webUrl}/-/commit/${commitHash}`;
|
||||||
|
case 'gitea':
|
||||||
|
return `${webUrl}/commit/${commitHash}`;
|
||||||
|
case 'azuredevops':
|
||||||
|
return `${webUrl}/commit/${commitHash}`;
|
||||||
|
case 'bitbucket-cloud':
|
||||||
|
return `${webUrl}/commits/${commitHash}`;
|
||||||
|
case 'bitbucket-server':
|
||||||
|
return `${webUrl}/commits/${commitHash}`;
|
||||||
|
case 'gerrit':
|
||||||
|
case 'generic-git-host':
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const isAuthSupportedForCodeHost = (codeHostType: CodeHostType): boolean => {
|
export const isAuthSupportedForCodeHost = (codeHostType: CodeHostType): boolean => {
|
||||||
switch (codeHostType) {
|
switch (codeHostType) {
|
||||||
case "github":
|
case "github":
|
||||||
|
|
@ -348,32 +380,38 @@ export const isDefined = <T>(arg: T | null | undefined): arg is T extends null |
|
||||||
return arg !== null && arg !== undefined;
|
return arg !== null && arg !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDisplayTime = (date: Date) => {
|
export const getFormattedDate = (date: Date) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const minutes = (now.getTime() - date.getTime()) / (1000 * 60);
|
const diffMinutes = (now.getTime() - date.getTime()) / (1000 * 60);
|
||||||
|
const isFuture = diffMinutes < 0;
|
||||||
|
|
||||||
|
// Use absolute values for calculations
|
||||||
|
const minutes = Math.abs(diffMinutes);
|
||||||
const hours = minutes / 60;
|
const hours = minutes / 60;
|
||||||
const days = hours / 24;
|
const days = hours / 24;
|
||||||
const months = days / 30;
|
const months = days / 30;
|
||||||
|
|
||||||
const formatTime = (value: number, unit: 'minute' | 'hour' | 'day' | 'month') => {
|
const formatTime = (value: number, unit: 'minute' | 'hour' | 'day' | 'month', isFuture: boolean) => {
|
||||||
const roundedValue = Math.floor(value);
|
const roundedValue = Math.floor(value);
|
||||||
if (roundedValue < 2) {
|
const pluralUnit = roundedValue === 1 ? unit : `${unit}s`;
|
||||||
return `${roundedValue} ${unit} ago`;
|
|
||||||
|
if (isFuture) {
|
||||||
|
return `In ${roundedValue} ${pluralUnit}`;
|
||||||
} else {
|
} else {
|
||||||
return `${roundedValue} ${unit}s ago`;
|
return `${roundedValue} ${pluralUnit} ago`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minutes < 1) {
|
if (minutes < 1) {
|
||||||
return 'just now';
|
return 'just now';
|
||||||
} else if (minutes < 60) {
|
} else if (minutes < 60) {
|
||||||
return formatTime(minutes, 'minute');
|
return formatTime(minutes, 'minute', isFuture);
|
||||||
} else if (hours < 24) {
|
} else if (hours < 24) {
|
||||||
return formatTime(hours, 'hour');
|
return formatTime(hours, 'hour', isFuture);
|
||||||
} else if (days < 30) {
|
} else if (days < 30) {
|
||||||
return formatTime(days, 'day');
|
return formatTime(days, 'day', isFuture);
|
||||||
} else {
|
} else {
|
||||||
return formatTime(months, 'month');
|
return formatTime(months, 'month', isFuture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue