add compare fork new pr, show forked from, resolve repo dids if given in navigation
1 unmodified line234566789102 unmodified lines1 unmodified line "client_id": "https://untangled.wisp.place/oauth-client-metadata.json", "client_name": "untangled", "client_uri": "https://untangled.wisp.place", "redirect_uris": ["https://untangled.wisp.place/oauth/callback"], "scope": "atproto repo:sh.tangled.actor.profile repo:sh.tangled.repo.issue repo:sh.tangled.repo.issue.comment repo:sh.tangled.repo.issue.state repo:sh.tangled.repo.pull repo:sh.tangled.repo.pull.comment repo:sh.tangled.repo.pull.status rpc:sh.tangled.repo.merge?aud=* blob:*/*", "scope": "atproto repo:sh.tangled.actor.profile repo:sh.tangled.repo.issue repo:sh.tangled.repo.issue.comment repo:sh.tangled.repo.issue.state repo:sh.tangled.repo.pull repo:sh.tangled.repo.pull.comment repo:sh.tangled.repo.pull.status rpc:sh.tangled.repo.merge?aud=* rpc:sh.tangled.repo.hiddenRef?aud=* blob:*/*", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", "application_type": "web",2 unmodified lines2319 unmodified linesExpand all232023212322232323242324232523262327232836 unmodified lines2365236623672368236923702371236923702371237223732374237519 unmodified lines2395239623972398239924002399240024012402240324041195 unmodified linesExpand all2319 unmodified linesExpand all}
.untangled-repo-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem; width: fit-content;}
36 unmodified lines align-items: center;}
.untangled-repo-action-main { gap: 0.5rem; min-width: 4.75rem; padding: 0 0.75rem; gap: 0.35rem; min-width: 3.5rem; padding: 0 0.5rem; flex: 1 1 auto; justify-content: center; position: relative; border: 0;19 unmodified lines.untangled-repo-action-count { border-left: 1px solid rgb(209 213 219); background: rgb(249 250 251); justify-content: center; min-width: 2.5rem; padding: 0 0.75rem; min-width: 2rem; padding: 0 0.5rem; position: relative; text-decoration: none;}
1195 unmodified linesExpand all30 unmodified lines313233343536373839404182 unmodified lines30 unmodified lines getRepoLog, getRepoTags, getRepoTree, listRepoRecords, compareFork, createHiddenRef, getRepoForkCount,} from './api/repos';export { createRepoStar, deleteRepoStar,82 unmodified lines21 unmodified lines222324252627282930195 unmodified linesExpand all22622722822923023123223323483 unmodified lines21 unmodified lines owner: string; slug: string; title: string; description?: string; isFork?: boolean; language?: string; stars?: number; openIssues?: number; openPRs?: number;195 unmodified linesExpand all owner: repo.owner.handle, slug: repo.slug, title: `${repo.owner.handle}/${repo.slug}`, description: repo.record.value.description?.trim() || undefined, isFork: Boolean(repo.record.value.source), language: existing?.language, stars: stats?.stars ?? existing?.stars, openIssues: stats?.openIssues ?? existing?.openIssues, openPRs: stats?.openPRs ?? existing?.openPRs,83 unmodified lines511 unmodified linesExpand all512513514515516517518519520281 unmodified linesExpand all511 unmodified linesExpand all href={`/${repo.owner}/${repo.slug}`} owner={repo.owner} name={repo.slug} description={repo.description} isFork={repo.isFork} language={repo.language} stars={repo.stars} openIssues={repo.openIssues} openPRs={repo.openPRs}281 unmodified linesExpand all16 unmodified lines17181920212122232425837 unmodified linesExpand all863864865866867868869870871872873871872873874874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917208 unmodified linesExpand all11261127112811291097113011311132113311341135113611011102110311371138113911401141114211431144114511461147101 unmodified linesExpand all12491250125112521253125412551256125759 unmodified lines13171318131913201321132213231324132521 unmodified lines13471348134913501351135213531354135577 unmodified lines16 unmodified lines X, Check, Pencil,} from 'lucide-solid';import { A, useParams, useSearchParams } from '@solidjs/router';import { A, useParams, useSearchParams, useNavigate } from '@solidjs/router';import { createQuery, useQueryClient } from '@tanstack/solid-query';import { For, Show, Switch, Match, createMemo, createSignal, createEffect, type Component } from 'solid-js';import type { Did } from '@atcute/lexicons/syntax';import { ErrorState, LoadingState, PlaceholderAvatar, textareaStyles, inputStyles } from '../components/common';837 unmodified linesExpand all const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const auth = useAuth(); const navigate = useNavigate(); const [repoSearch, setRepoSearch] = createSignal(''); const [actionLoading, setActionLoading] = createSignal(false); const [isRedirecting, setIsRedirecting] = createSignal(false);
const actorParam = () => params.actor as string; const activeTab = () => { const tab = searchParams.tab; return (Array.isArray(tab) ? tab[0] : tab) || 'overview'; };
createEffect(() => { actorParam(); setIsRedirecting(false); });
const isDid = () => actorParam().startsWith('did:');
const actorQuery = createQuery(() => ({ queryKey: ['profile-actor', actorParam()], queryFn: () => resolveActor(actorParam()), }));
const repoDidQuery = createQuery(() => ({ queryKey: ['repo-by-did', actorParam()], enabled: isDid(), queryFn: () => getRepoByDid(actorParam() as Did), retry: false, }));
createEffect(() => { const repo = repoDidQuery.data; if (repo) { setIsRedirecting(true); navigate(`/${repo.owner.handle}/${repo.slug}`, { replace: true }); } });
createEffect(() => { const err = repoDidQuery.error; if (err) { console.error('Failed to resolve DID as repo:', err); } });
const activeTab = () => { const tab = searchParams.tab; return (Array.isArray(tab) ? tab[0] : tab) || 'overview'; };
const profileQuery = createQuery(() => ({ queryKey: ['profile-record', actorQuery.data?.did], enabled: !!actorQuery.data, queryFn: () => getActorProfile(actorQuery.data!),208 unmodified linesExpand all
return ( <Switch> <Match when={actorQuery.isLoading}> <LoadingState label="Loading profile..." /> <LoadingState label={isDid() ? "Resolving identifier..." : "Loading profile..."} /> </Match>
<Match when={actorQuery.error}> <Show when={isRedirecting() || (isDid() && repoDidQuery.isLoading)} fallback={ <div class="p-6 border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-800"> <ErrorState message="Could not resolve user profile." /> </div> <div class="p-6 border border-gray-200 dark:border-gray-700 rounded bg-white dark:bg-gray-800"> <ErrorState message="Could not resolve user profile." /> </div> } > <LoadingState label="Resolving repository DID..." /> </Show> </Match>
<Match when={actorQuery.data}> {(resolvedActor) => (101 unmodified linesExpand all <RepoCard owner={resolvedActor().handle} name={repo.value.name || repo.rkey} description={repo.value.description} isFork={Boolean(repo.value.source)} showOwner={false} /> )} </For>59 unmodified lines <RepoCard owner={resolvedActor().handle} name={repo.value.name || repo.rkey} description={repo.value.description} isFork={Boolean(repo.value.source)} showOwner={false} /> )} </For>21 unmodified lines <RepoCard owner={repo.owner.handle} name={repo.slug} description={repo.record.value.description} isFork={Boolean(repo.record.value.source)} showOwner={true} /> )} </For>77 unmodified lines122345675 unmodified lines828384858687888990271 unmodified linesExpand all362363364365366367368369370114 unmodified linesExpand allimport { ok } from '@atcute/client';import type { ResourceUri } from '@atcute/lexicons/syntax';import type { Did, ResourceUri } from '@atcute/lexicons/syntax';import type { OAuthUserAgent } from '@atcute/oauth-browser-client';import { ShTangledRepoMerge, ShTangledRepoPull,75 unmodified lines body: string; targetBranch: string; sourceBranch: string; patch: string; sourceRepoDid?: Did;}
export interface MergePullInput { pull: PullSummary;271 unmodified linesExpand all branch: input.targetBranch, }, source: { branch: input.sourceBranch, repo: input.sourceRepoDid, }, rounds: [ { createdAt: new Date().toISOString(),114 unmodified linesExpand all9 unmodified lines101112131414151617181920588 unmodified linesExpand all6096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636649 unmodified lines getRepoViaAppview, listAllAppviewRecords, type AppviewRecordView,} from './appview';import { getRpc, normalizeServiceUrl } from './client';import { getBacklinksPage } from './backlinks';import type { OAuthUserAgent } from '@atcute/oauth-browser-client';import { getRpc, normalizeServiceUrl, createServiceAuthRpc } from './client';import { REPO_COLLECTION } from './constants';import { resolveActor } from './identity';import { maxTimestamp,588 unmodified linesExpand all }
return `data:${blob.mimeType};charset=utf-8,${encodeURIComponent(blob.content)}`;};
export const createHiddenRef = async ( agent: OAuthUserAgent, forkKnot: string, input: { forkRef: string; remoteRef: string; repo: ResourceUri; },): Promise<void> => { const rpc = await createServiceAuthRpc(agent, forkKnot, 'sh.tangled.repo.hiddenRef'); await ok( rpc.post('sh.tangled.repo.hiddenRef', { input: { forkRef: input.forkRef, remoteRef: input.remoteRef, repo: input.repo, }, }), );};
export const compareFork = async ( agent: OAuthUserAgent, forkRepo: RepoContext, targetBranch: string, sourceBranch: string,): Promise<CompareResponse> => { await createHiddenRef(agent, forkRepo.knot, { forkRef: sourceBranch, remoteRef: targetBranch, repo: forkRepo.record.uri, });
const hiddenRef = `hidden/${sourceBranch}/${targetBranch}`; return compareBranches(forkRepo, hiddenRef, sourceBranch);};
export const getRepoForkCount = async (repo: RepoContext): Promise<number> => { try { const source = 'sh.tangled.repo:source'; const [didPage, uriPage] = await Promise.all([ getBacklinksPage(repo.repoDid, source, 1), getBacklinksPage(repo.record.uri, source, 1), ]); const didCount = didPage.total ?? didPage.records.length; const uriCount = uriPage.total ?? uriPage.records.length; return didCount + uriCount; } catch { return 0; }};12344567869 unmodified linesimport type { SearchParamValue } from '../../lib/repo-utils';
export type PullStateFilter = 'open' | 'closed' | 'merged';export type PullSourceMode = 'branch' | 'patch';export type PullSourceMode = 'branch' | 'fork' | 'patch';
export interface PullFilter { state: PullStateFilter; author?: string;69 unmodified lines13 unmodified lines1415161718192021222324242526272829214 unmodified linesExpand all24424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832929 unmodified lines3593603613623633643653663673683693703713723733743753763773783792 unmodified lines3823833843853863873883893903913923933943953963973981 unmodified line40040140240330330430540440540640740840941041141241341441541641741841942042142218 unmodified lines4414424434444454464474484494504514524534544554564574584 unmodified lines46346446546634434534634734834935035135235335435535635735835946746846947047147247347447547647747847948048148248348448548648713 unmodified lines50150250350450550650750850951051151251351476 unmodified lines59159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367447 unmodified lines72272372472572672772872973073173273373473573673773873910 unmodified lines750751752753754755756757758759760761762763545764765766767768769770771772773774775776777554778779780781782783784620 unmodified linesExpand all13 unmodified lines RefreshCcwDot, Search, X,} from 'lucide-solid';import type { Did } from '@atcute/lexicons/syntax';import { A, useNavigate, useParams, useSearchParams } from '@solidjs/router';import { createQuery, useQueryClient } from '@tanstack/solid-query';import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js';import { resolveActor } from '../../lib/api/identity';import { createPull, createPullComment, fetchPullRoundPatch, getPull, listPullsPage, mergePull, setPullStatus } from '../../lib/api/pulls';import { parseAtUri } from '../../lib/api/records';import { compareBranches, getRepoBranches, type RepoContext } from '../../lib/api/repos';import { compareBranches, getRepoBranches, type RepoContext, compareFork, listRepoRecords } from '../../lib/api/repos';import { useAuth } from '../../lib/auth';import { Avatar, ErrorState, LoadingState, PaginationControls, StateBadge, ToggleButton, buttonStyles, cardStyles, textareaStyles } from '../../components/common';import { AtUriPanel, BranchPill, CommentCard, DiffView, MarkdownBlock, PullComposeSkeleton, PullDiffSkeleton, PullDiffView, RepoListPageSkeleton, RepoListSkeleton, RepoThreadSkeleton } from '../../components/repo';import { RepoFrame, pullQueryKey, pullsQueryKey, useRepoQuery } from './shared';214 unmodified linesExpand all const [previewingBody, setPreviewingBody] = createSignal(false); const [error, setError] = createSignal<string | null>(null); const [submitting, setSubmitting] = createSignal(false);
const [selectedForkDid, setSelectedForkDid] = createSignal(''); const [forkSourceBranch, setForkSourceBranch] = createSignal('');
const viewerQuery = createQuery(() => { return { queryKey: ['viewer', auth.currentDid()], enabled: Boolean(auth.currentDid()), queryFn: async () => resolveActor(auth.currentDid()!), }; });
const myReposQuery = createQuery(() => { return { queryKey: ['myRepos', auth.currentDid()], enabled: Boolean(auth.currentDid() && sourceMode() === 'fork'), queryFn: async () => listRepoRecords(auth.currentDid()!), }; });
const forks = createMemo(() => { const repos = myReposQuery.data ?? []; const currentRepo = repoQuery.data; if (!currentRepo) return []; return repos.filter((r) => { const source = r.value.source; if (!source) return false; return source === currentRepo.repoDid || source === currentRepo.record.uri; }); });
const selectedForkRecord = createMemo(() => { return forks().find((f) => f.value.repoDid === selectedForkDid()); });
createEffect(() => { const availableForks = forks(); if (availableForks.length > 0 && !selectedForkDid()) { setSelectedForkDid(availableForks[0].value.repoDid!); } });
createEffect(() => { selectedForkDid(); setForkSourceBranch(''); });
const selectedForkContext = createMemo<RepoContext | null>(() => { const forkRec = selectedForkRecord(); const viewer = viewerQuery.data; if (!forkRec || !viewer) return null; return { owner: viewer, record: forkRec, repoDid: forkRec.value.repoDid!, slug: forkRec.value.name || forkRec.rkey, rkey: forkRec.rkey, knot: forkRec.value.knot, }; });
const forkBranchesQuery = createQuery(() => { const forkCtx = selectedForkContext(); return { queryKey: ['branches', forkCtx?.repoDid], enabled: Boolean(sourceMode() === 'fork' && forkCtx), queryFn: async () => getRepoBranches(forkCtx!), }; });
createEffect(() => { const branches = forkBranchesQuery.data?.branches ?? []; if (branches.length === 0) return; if (!forkSourceBranch()) { const fallback = branches.find((branch) => branch.is_default)?.reference.name || branches[0].reference.name; setForkSourceBranch(fallback); } });
const branchesQuery = createQuery(() => { const repo = repoQuery.data; return { queryKey: ['branches', repo?.repoDid],29 unmodified lines queryFn: async () => compareBranches(repo!, target, source), }; });
const forkCompareQuery = createQuery(() => { const agent = auth.agent(); const forkCtx = selectedForkContext(); const target = targetBranch(); const source = forkSourceBranch(); const mode = sourceMode(); return { queryKey: ['compare-fork', forkCtx?.repoDid, target, source], enabled: Boolean(mode === 'fork' && agent && forkCtx && target && source), queryFn: async () => compareFork(agent!, forkCtx!, target, source), }; });
createEffect(() => { if (sourceMode() !== 'branch') return; const comparison = compareQuery.data; if (!comparison || comparison.format_patch.length === 0) return;2 unmodified lines if (!touchedBody()) setBody(patch.Body || ''); });
createEffect(() => { if (sourceMode() !== 'fork') return; const comparison = forkCompareQuery.data; if (!comparison || comparison.format_patch.length === 0) return; const patch = comparison.format_patch[0]; if (!touchedTitle()) setTitle(patch.Title || ''); if (!touchedBody()) setBody(patch.Body || ''); });
createEffect(() => { if (sourceMode() !== 'patch') return; const prefill = parsePastedPatchPrefill(patchText()); if (!prefill.title) return; if (!touchedTitle()) setTitle(prefill.title);1 unmodified line });
const pastedPatch = createMemo(() => patchText().trim()); const hasPastedPatch = createMemo(() => sourceMode() === 'patch' && Boolean(pastedPatch())); const activePatch = createMemo(() => (sourceMode() === 'patch' ? pastedPatch() : compareQuery.data?.patch ?? '')); const hasComparison = createMemo(() => sourceMode() === 'branch' && Boolean(compareQuery.data)); const commits = createMemo(() => (sourceMode() === 'branch' ? (compareQuery.data?.format_patch ?? []) : [])); const activePatch = createMemo(() => { if (sourceMode() === 'patch') return pastedPatch(); if (sourceMode() === 'fork') return forkCompareQuery.data?.patch ?? ''; return compareQuery.data?.patch ?? ''; }); const hasComparison = createMemo(() => { if (sourceMode() === 'branch') return Boolean(compareQuery.data); if (sourceMode() === 'fork') return Boolean(forkCompareQuery.data); return false; }); const commits = createMemo(() => { if (sourceMode() === 'branch') return compareQuery.data?.format_patch ?? []; if (sourceMode() === 'fork') return forkCompareQuery.data?.format_patch ?? []; return []; }); const hasCommits = createMemo(() => commits().length > 0); const hasPatch = createMemo(() => Boolean(activePatch())); const showDetails = createMemo(() => hasCommits() || hasPatch());
18 unmodified lines setError('Source branch required.'); return; }
if (mode === 'fork' && !selectedForkDid()) { setError('Fork selection required.'); return; }
if (mode === 'fork' && !forkSourceBranch()) { setError('Fork branch required.'); return; }
if (!patch) { setError(mode === 'patch' ? 'Paste a patch first.' : 'No patch available for the selected branches.'); return; }4 unmodified lines }
setSubmitting(true); setError(null); try { const created = await createPull(agent, repo, { title: title(), body: body(), sourceBranch: mode === 'patch' ? targetBranch() : sourceBranch(), targetBranch: targetBranch(), patch, }); await client.invalidateQueries({ queryKey: pullsQueryKey(repo.repoDid) }); navigate(pullHref(repo, created.uri)); } catch (cause) { setError(cause instanceof Error ? cause.message : 'Failed to create pull request'); } finally { setSubmitting(false); } }; try { const created = await createPull(agent, repo, { title: title(), body: body(), sourceBranch: mode === 'patch' ? targetBranch() : (mode === 'fork' ? forkSourceBranch() : sourceBranch()), targetBranch: targetBranch(), patch, sourceRepoDid: mode === 'fork' ? (selectedForkDid() as Did) : undefined, }); await client.invalidateQueries({ queryKey: pullsQueryKey(repo.repoDid) }); navigate(pullHref(repo, created.uri)); } catch (cause) { setError(cause instanceof Error ? cause.message : 'Failed to create pull request'); } finally { setSubmitting(false); } };
return ( <RepoFrame active="pulls" loadingFallback={<PullComposeSkeleton />}> <div id="pr-compose-host" class="flex flex-col bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full mx-auto dark:text-white">13 unmodified lines description="Select a source branch" onClick={() => setSourceMode('branch')} /> <PullSourceTab active={sourceMode() === 'fork'} title="Compare forks" description="Select a fork and branch" onClick={() => setSourceMode('fork')} /> <PullSourceTab active={sourceMode() === 'patch'} title="Paste patch" description={ <span>76 unmodified lines </Show> </div> </div> </Show>
<Show when={sourceMode() === 'fork'}> <div id="patch-strategy" class="flex flex-col gap-1"> <label for="forkSelect" class="text-xs uppercase tracking-wide text-gray-800 dark:text-gray-200"> Pull from </label> <div class="flex flex-wrap gap-2 items-center"> <select id="forkSelect" name="fork" required value={selectedForkDid()} onChange={(event) => setSelectedForkDid(event.currentTarget.value)} class="peer p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" > <option disabled selected={!selectedForkDid()}> Select a fork </option> <For each={forks()}> {(fork) => ( <option value={fork.value.repoDid}> {viewerQuery.data?.handle ?? auth.currentDid()}/{fork.value.name || fork.rkey} </option> )} </For> </select> <Show when={myReposQuery.isLoading || viewerQuery.isLoading}> <LoaderCircle class="size-4 animate-spin text-gray-500 dark:text-gray-400" /> </Show> </div> </div>
<Show when={selectedForkDid()}> <div class="flex flex-col gap-1"> <label for="forkSourceBranch" class="text-xs uppercase tracking-wide text-gray-800 dark:text-gray-200"> Fork Branch </label> <div class="flex flex-wrap gap-2 items-center"> <select id="forkSourceBranch" name="forkSourceBranch" required value={forkSourceBranch()} onChange={(event) => setForkSourceBranch(event.currentTarget.value)} class="peer p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" > <option disabled selected={!forkSourceBranch()}> Source branch </option> <For each={forkBranchesQuery.data?.branches ?? []}> {(branch, index) => { const name = branch.reference.name; return ( <option value={name}> {name} {index() === 0 ? ' (new)' : ''} </option> ); }} </For> </select> <Show when={forkCompareQuery.isFetching || forkBranchesQuery.isLoading}> <LoaderCircle class="size-4 animate-spin text-gray-500 dark:text-gray-400" /> </Show> </div> </div> </Show> </Show> </div>
<Show when={sourceMode() === 'fork' && forks().length === 0 && !myReposQuery.isLoading && !viewerQuery.isLoading}> <div class="text-sm text-gray-500 dark:text-gray-400"> No forks found for this repository. </div> </Show>
<Show when={sourceMode() === 'patch'}> <div id="patch-upload"> <div class="flex items-center justify-between gap-2"> <p class="my-0">47 unmodified lines <div class="p-4 border border-red-200 dark:border-red-800 rounded bg-red-50 dark:bg-gray-800"> <ErrorState message={getErrorMessage(compareQuery.error)} /> </div> </Match> <Match when={sourceMode() === 'fork' && forkCompareQuery.isLoading}> <div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800"> <LoadingState label="Generating comparison..." /> </div> </Match> <Match when={sourceMode() === 'fork' && forkCompareQuery.error}> <div class="p-4 border border-red-200 dark:border-red-800 rounded bg-red-50 dark:bg-gray-800"> <ErrorState message={getErrorMessage(forkCompareQuery.error)} /> </div> </Match> <Match when={sourceMode() === 'patch' && !hasPastedPatch()}> <div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400"> Paste a patch above to see a comparison. </div>10 unmodified lines <div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400"> Pick a source and target above to see a comparison. </div> </Match> <Match when={sourceMode() === 'fork' && (!selectedForkDid() || !forkSourceBranch())}> <div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400"> Pick a fork and source branch above to see a comparison. </div> </Match> <Match when={sourceMode() === 'branch' && hasComparison() && !hasCommits()}> <div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400"> No commits between target and source. Make sure your source branch has commits not on the target. </div> </Match> <Match when={sourceMode() === 'branch' && hasCommits()}> <Match when={sourceMode() === 'fork' && hasComparison() && !hasCommits()}> <div class="p-4 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400"> No commits between target and source. Make sure your source branch has commits not on the target. </div> </Match> <Match when={(sourceMode() === 'branch' || sourceMode() === 'fork') && hasCommits()}> <div class="flex flex-col gap-2"> <div class="flex items-center justify-between gap-3 min-w-0 text-sm text-gray-500 dark:text-gray-400"> <span class="flex-shrink-0"> {commits().length} commit{commits().length === 1 ? '' : 's'} </span> <span class="inline-flex items-center gap-2 font-mono text-gray-600 dark:text-gray-300 truncate"> <span>{targetBranch()}</span> <ArrowLeftRight class="w-4 h-4 flex-shrink-0" /> <span>{sourceBranch()}</span> <span> {sourceMode() === 'fork' ? `${viewerQuery.data?.handle ?? auth.currentDid()}/${selectedForkRecord()?.value.name || selectedForkRecord()?.rkey}:${forkSourceBranch()}` : sourceBranch()} </span> </span> </div>
<ul class="flex flex-col gap-2">620 unmodified linesExpand all