mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +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
|
|
@ -268,4 +268,16 @@ export const getTags = async (path: string) => {
|
|||
const git = createGitClientForPath(path);
|
||||
const tags = await git.tags();
|
||||
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 { INDEX_CACHE_DIR } from './constants.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 { repoMetadataSchema, RepoWithConnections, Settings } from "./types.js";
|
||||
import { getAuthCredentialsForRepo, getRepoPath, getShardPrefix, groupmqLifecycleExceptionWrapper, measure } from './utils.js';
|
||||
|
|
@ -384,16 +384,26 @@ export class RepoIndexManager {
|
|||
data: {
|
||||
status: RepoIndexingJobStatus.COMPLETED,
|
||||
completedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
repo: true,
|
||||
}
|
||||
});
|
||||
|
||||
const jobTypeLabel = getJobTypePrometheusLabel(jobData.type);
|
||||
|
||||
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({
|
||||
where: { id: jobData.repoId },
|
||||
data: {
|
||||
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[]
|
||||
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_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
|
||||
} from "react-icons/vsc";
|
||||
import { useSearchHistory } from "@/hooks/useSearchHistory";
|
||||
import { getDisplayTime, isServiceError, unwrapServiceError } from "@/lib/utils";
|
||||
import { getFormattedDate, isServiceError, unwrapServiceError } from "@/lib/utils";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ export const useSuggestionsData = ({
|
|||
const searchHistorySuggestions = useMemo(() => {
|
||||
return searchHistory.map(search => ({
|
||||
value: search.query,
|
||||
description: getDisplayTime(new Date(search.date)),
|
||||
description: getFormattedDate(new Date(search.date)),
|
||||
} satisfies Suggestion));
|
||||
}, [searchHistory]);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,17 +16,7 @@ import { Suspense } from "react"
|
|||
import { RepoJobsTable } from "../components/repoJobsTable"
|
||||
import { getConfigSettings } from "@sourcebot/shared"
|
||||
import { env } from "@/env.mjs"
|
||||
|
||||
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)
|
||||
}
|
||||
import { DisplayDate } from "../../components/DisplayDate"
|
||||
|
||||
export default async function RepoDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
|
|
@ -109,7 +99,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold">{formatDate(repo.createdAt)}</div>
|
||||
<DisplayDate date={repo.createdAt} className="text-2xl font-semibold"/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -128,7 +118,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Card>
|
||||
|
||||
|
|
@ -147,7 +137,7 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-semibold">{nextIndexAttempt ? formatDate(nextIndexAttempt) : "-"}</div>
|
||||
{nextIndexAttempt ? <DisplayDate date={nextIndexAttempt} className="text-2xl font-semibold"/> : "-" }
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { useMemo } from "react"
|
|||
import { LightweightCodeHighlighter } from "../../components/lightweightCodeHighlighter"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useToast } from "@/components/hooks/use-toast"
|
||||
import { DisplayDate } from "../../components/DisplayDate"
|
||||
|
||||
// @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) => {
|
||||
if (!end) return "-"
|
||||
const diff = end.getTime() - start.getTime()
|
||||
|
|
@ -139,7 +129,7 @@ export const columns: ColumnDef<RepoIndexingJob>[] = [
|
|||
</Button>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => formatDate(row.getValue("createdAt")),
|
||||
cell: ({ row }) => <DisplayDate date={row.getValue("createdAt") as Date} className="ml-3"/>,
|
||||
},
|
||||
{
|
||||
accessorKey: "completedAt",
|
||||
|
|
@ -151,7 +141,14 @@ export const columns: ColumnDef<RepoIndexingJob>[] = [
|
|||
</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",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { Input } from "@/components/ui/input"
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
|
||||
import { getCodeHostInfoForRepo, getRepoImageSrc } from "@/lib/utils"
|
||||
import { CodeHostType, getCodeHostCommitUrl, getCodeHostInfoForRepo, getFormattedDate, getRepoImageSrc } from "@/lib/utils"
|
||||
import {
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
|
|
@ -35,6 +35,7 @@ import { useMemo, useState } from "react"
|
|||
import { getBrowsePath } from "../../browse/hooks/utils"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { DisplayDate } from "../../components/DisplayDate"
|
||||
|
||||
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ export type Repo = {
|
|||
webUrl: string | null
|
||||
codeHostType: string
|
||||
imageUrl: string | null
|
||||
indexedCommitHash: string | null
|
||||
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
||||
}
|
||||
|
||||
|
|
@ -81,13 +83,7 @@ const getStatusBadge = (status: Repo["latestJobStatus"]) => {
|
|||
|
||||
const 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)
|
||||
return getFormattedDate(date);
|
||||
}
|
||||
|
||||
export const columns: ColumnDef<Repo>[] = [
|
||||
|
|
@ -132,20 +128,64 @@ export const columns: ColumnDef<Repo>[] = [
|
|||
},
|
||||
{
|
||||
accessorKey: "latestJobStatus",
|
||||
header: "Status",
|
||||
header: "Lastest status",
|
||||
cell: ({ row }) => getStatusBadge(row.getValue("latestJobStatus")),
|
||||
},
|
||||
{
|
||||
accessorKey: "indexedAt",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
|
||||
Last Indexed
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Last synced
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</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",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default async function ReposPage() {
|
|||
imageUrl: repo.imageUrl,
|
||||
latestJobStatus: repo.jobs.length > 0 ? repo.jobs[0].status : null,
|
||||
codeHostType: repo.external_codeHostType,
|
||||
indexedCommitHash: repo.indexedCommitHash,
|
||||
}))} />
|
||||
</div>
|
||||
)
|
||||
|
|
@ -44,6 +45,9 @@ const getReposWithLatestJob = async () => sew(() =>
|
|||
},
|
||||
take: 1
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
});
|
||||
return repos;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
|
|||
import { LucideKeyRound, MoreVertical, Search, LucideTrash } from "lucide-react";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
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 { Button } from "@/components/ui/button";
|
||||
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 className="flex items-center gap-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Created {getDisplayTime(secret.createdAt)}
|
||||
Created {getFormattedDate(secret.createdAt)}
|
||||
</p>
|
||||
<DropdownMenu modal={false}>
|
||||
<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 => {
|
||||
switch (codeHostType) {
|
||||
case "github":
|
||||
|
|
@ -348,32 +380,38 @@ export const isDefined = <T>(arg: T | null | undefined): arg is T extends null |
|
|||
return arg !== null && arg !== undefined;
|
||||
}
|
||||
|
||||
export const getDisplayTime = (date: Date) => {
|
||||
export const getFormattedDate = (date: 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 days = hours / 24;
|
||||
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);
|
||||
if (roundedValue < 2) {
|
||||
return `${roundedValue} ${unit} ago`;
|
||||
const pluralUnit = roundedValue === 1 ? unit : `${unit}s`;
|
||||
|
||||
if (isFuture) {
|
||||
return `In ${roundedValue} ${pluralUnit}`;
|
||||
} else {
|
||||
return `${roundedValue} ${unit}s ago`;
|
||||
return `${roundedValue} ${pluralUnit} ago`;
|
||||
}
|
||||
}
|
||||
|
||||
if (minutes < 1) {
|
||||
return 'just now';
|
||||
} else if (minutes < 60) {
|
||||
return formatTime(minutes, 'minute');
|
||||
return formatTime(minutes, 'minute', isFuture);
|
||||
} else if (hours < 24) {
|
||||
return formatTime(hours, 'hour');
|
||||
return formatTime(hours, 'hour', isFuture);
|
||||
} else if (days < 30) {
|
||||
return formatTime(days, 'day');
|
||||
return formatTime(days, 'day', isFuture);
|
||||
} else {
|
||||
return formatTime(months, 'month');
|
||||
return formatTime(months, 'month', isFuture);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue