Store HEAD commit hash in DB. Also improve how we display dates

This commit is contained in:
bkellam 2025-10-23 17:11:22 -07:00
parent cd54eb0b7b
commit 8168ec2c1f
12 changed files with 184 additions and 54 deletions

View file

@ -268,4 +268,16 @@ export const getTags = async (path: string) => {
const git = createGitClientForPath(path); const git = createGitClientForPath(path);
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;
} }

View file

@ -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,
} }
}); });

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Repo" ADD COLUMN "indexedCommitHash" TEXT;

View file

@ -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.)

View 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>
)
}

View file

@ -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]);

View file

@ -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>

View file

@ -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",

View file

@ -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",

View file

@ -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;

View file

@ -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>

View file

@ -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);
} }
} }