// The directory behind the source view's left pane. // // Both halves of the view want the same listing: the pane lists it, and a // directory's own page reads it again to find the README. One query key // serves both, so opening a file beside a listing costs no second walk. import { creditAuthors } from '@pdsjs/git/authors'; import { useQuery } from '@tanstack/react-query'; import { ACTIVITY_WINDOW_MS } from './activity.js'; import { pullCommits } from './collab.js'; import { account, isEmpty, lastCommitOf, readerFor, repoRecord, } from './git.js'; /** Deep enough to count and date every commit a repository of this shape holds. */ const COUNT_LIMIT = 50000; /** * The directory holding a path. A path with no slash is at the root, whose * parent is the root itself. * @param {string} path * @returns {string} */ export function parentOf(path) { const cut = path.lastIndexOf('/'); return cut === -1 ? '' : path.slice(0, cut); } /** * One directory's entries at one ref. An empty repository has none, and asks * for nothing. * @param {string} repo * @param {string} refName * @param {string} path - the directory, '' for the root * @param {string} [space] - the space holding a private repository */ export function useDirectory(repo, refName, path, space) { const record = repoRecord(repo, space); return useQuery({ queryKey: directoryKey(repo, refName, path, space), enabled: Boolean(record) && !isEmpty(record), queryFn: () => readDirectory(repo, refName, path, space), }); } /** * Every file at one ref, with its size, as one listing. The rail draws the * whole tree from it and the language gauge counts it, so one read serves * both for as long as the ref does. * @param {string} repo * @param {string} refName * @param {string} [space] - the space holding a private repository */ export function useFileList(repo, refName, space) { const record = repoRecord(repo, space); return useQuery({ queryKey: ['files', repo, refName, space ?? ''], enabled: Boolean(record) && !isEmpty(record), queryFn: async () => { const listing = await readerFor(repo, undefined, space).listFiles( account.did, repo, refName, ); return listing?.files ?? []; }, staleTime: Number.POSITIVE_INFINITY, }); } /** * The last commit to change one file. The answer holds for as long as the * ref does, so one read serves every visit to the file in a session. * @param {string} repo * @param {string} refName * @param {string} path * @param {string} [space] - the space holding a private repository */ export function useFileCommit(repo, refName, path, space) { const record = repoRecord(repo, space); return useQuery({ queryKey: ['file-commit', repo, refName, path, space ?? ''], enabled: Boolean(record) && !isEmpty(record) && Boolean(path), queryFn: () => lastCommitOf(repo, refName, path, space), staleTime: Number.POSITIVE_INFINITY, }); } /** * The tip of a ref, how far its history runs, and when its recent commits * were made. The walk reads every commit, which on an indexed chain is a * download of its own, so a caller paints first and fills this in. * * Three readers want the same answer: the latest-commit line, the commit * count beside it, and the activity gauge. One key serves them all. * @param {string} repo * @param {string} refName * @param {string} [space] - the space holding a private repository */ export function useHistory(repo, refName, space) { const record = repoRecord(repo, space); return useQuery({ queryKey: historyKey(repo, refName, space), enabled: Boolean(record) && !isEmpty(record), queryFn: () => readHistory(repo, refName, space), }); } /** * @param {string} repo @param {string} refName @param {string} path * @param {string} [space] */ export const directoryKey = (repo, refName, path, space) => [ 'dir', repo, refName, path, space ?? '', ]; /** * @param {string} repo @param {string} refName @param {string} path * @param {string} [space] */ export async function readDirectory(repo, refName, path, space) { const listing = await readerFor(repo, undefined, space).listTree( account.did, repo, refName, path, ); return listing?.entries ?? []; } /** * @param {string} repo @param {string} refName @param {string} [space] */ export const historyKey = (repo, refName, space) => [ 'history', repo, refName, space ?? '', ]; /** * The tip of a ref, how far its history runs, and when its recent commits * were made. * @param {string} repo @param {string} refName @param {string} [space] */ export async function readHistory(repo, refName, space) { const log = await readerFor(repo, undefined, space).listCommits( account.did, repo, refName, COUNT_LIMIT, ); const floor = Date.now() - ACTIVITY_WINDOW_MS; /** @type {number[]} */ const dates = []; const commits = log?.commits ?? []; for (const commit of commits) { if (commit.committedAt >= floor) dates.push(commit.committedAt); } return { head: commits[0] ?? null, count: commits.length, truncated: Boolean(log?.truncated), dates, authors: creditAuthors(commits), }; } /** * @param {string} repo @param {string} refName @param {string} path * @param {string} [space] */ export const fileKey = (repo, refName, path, space) => [ 'file', repo, refName, path, space ?? '', ]; /** * One file at a ref, with its bytes where it is an image. * @param {string} repo @param {string} refName @param {string} path * @param {string} [space] */ export async function readSource(repo, refName, path, space) { const reader = readerFor(repo, undefined, space); const file = await reader.readFile(account.did, repo, refName, path); const image = file?.mediaType ? await reader.readImage(account.did, repo, refName, path) : null; return { file, image }; } /** * @param {string} repo @param {string|undefined} refName * @param {string|undefined} range @param {string|undefined} from * @param {string} [space] @param {string} [path] */ export const logKey = (repo, refName, range, from, space, path) => [ 'commits', repo, refName ?? '', range ?? '', from ?? '', space ?? '', path ?? '', ]; /** * A history: a ref's, one path's along it, or the commits across a range in * whichever copy holds them. * @param {string} repo * @param {{refName?: string, range?: string, from?: string, limit: number, space?: string, path?: string}} what */ export async function readLog( repo, { refName, range, from, limit, space, path }, ) { if (!range) { return readerFor(repo, undefined, space).listCommits( account.did, repo, refName, limit, path, ); } const [base, tip] = range.split('..'); return pullCommits(repo, base || null, tip, from ?? '', limit); }