chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQueryClient } from '@tanstack/react-query';
import { CircleCheckIcon, GitMergeIcon, TriangleAlertIcon } from 'lucide-react';
import { useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { PathList } from '#/components/molecules/path-list.jsx';
import { checkLook, summarize } from '#/lib/checks.js';
import { account, loadRepos } from '#/lib/git.js';
import { mergePull } from '#/lib/merge.js';
import { useSession } from '#/lib/session.jsx';
/**
* Whether the pull request can merge, and the merge itself.
*
* The answer is the server's three-way merge of the branch and main against
* their base, made when the list was read: clean, or the paths where the two
* changed the same thing differently. Everybody reads the answer. The owner
* of the repository gets the buttons, because merging writes the canonical
* branch and nobody else may.
*/
export function MergeBox({ repo, pull, checks, checksHref }) {
const { session } = useSession();
const queryClient = useQueryClient();
const [busy, setBusy] = useState('');
const [error, setError] = useState('');
const [merged, setMerged] = useState('');
if (pull.merged) return null;
const mine = session?.did === account.did;
const conflicts = pull.conflicts ?? [];
const merge = async (mode) => {
setBusy(mode);
setError('');
try {
const answer = await mergePull(session, repo, pull, mode);
setMerged(answer.sha);
// Main moved. The repository record the whole app reads from names the
// old tip, so it is read again before the reads that hang off it are.
// Then every read is marked stale: the pull request is merged, its
// commits are on main, and main's history, tree and list have changed.
await loadRepos();
await queryClient.invalidateQueries();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setBusy('');
}
};
return (
<div className="px-(--gutter) py-2">
<div className="flex flex-col gap-3 rounded-md border border-border p-4">
{(() => {
const summary = summarize(checks ?? []);
if (!summary) return null;
const look = checkLook(summary.status);
return (
<div className="flex items-center gap-1.5 border-border border-b pb-3 text-[13px]">
<look.Icon className={`size-4 shrink-0 ${look.className}`} />
<span className="font-medium text-foreground">
{summary.passed} of {summary.total}{' '}
{summary.total === 1 ? 'check' : 'checks'} passed
</span>
{checksHref && (
<Link href={checksHref} className="ml-auto text-[12.5px]">
Details
</Link>
)}
</div>
);
})()}
{merged ? (
<p className="flex items-center gap-1.5 text-[13px] text-success">
<GitMergeIcon className="size-4 shrink-0" />
Merged as <code className="font-mono">{merged.slice(0, 8)}</code>.
</p>
) : conflicts.length > 0 ? (
<div className="flex flex-col gap-1.5">
<p className="flex items-center gap-1.5 font-medium text-[13px] text-destructive">
<TriangleAlertIcon className="size-4 shrink-0" />
This branch has conflicts that must be resolved.
</p>
<div className="pl-5.5 text-[12.5px] text-muted-foreground">
<PathList paths={conflicts} label="Conflicting files:" />
</div>
{mine && (
<p className="pl-5.5 text-[12.5px] text-faint">
Merge main into the branch, or rebase it, and push again.
</p>
)}
</div>
) : !pull.rooted ? (
<p className="text-[12.5px] text-faint">
This branch shares no history with main, so there is nothing to
merge it onto.
</p>
) : pull.conflictsUnknown ? (
<p className="flex items-center gap-1.5 text-[13px] text-muted-foreground">
<TriangleAlertIcon className="size-4 shrink-0 text-faint" />
This branch changes many of the files main has changed. Merging
reads them all and stops if any conflict.
</p>
) : (
<p className="flex items-center gap-1.5 font-medium text-[13px] text-success">
<CircleCheckIcon className="size-4 shrink-0" />
This branch has no conflicts with main.
{pull.behind > 0 && (
<span className="font-normal text-faint">
Main has moved {pull.behind}{' '}
{pull.behind === 1 ? 'commit' : 'commits'} since it branched.
</span>
)}
</p>
)}
{mine && !merged && conflicts.length === 0 && pull.rooted && (
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
disabled={Boolean(busy)}
onClick={() => merge('merge')}
>
<GitMergeIcon className="size-3.5" />
{busy === 'merge' ? 'Merging…' : 'Merge'}
</Button>
<Button
size="sm"
variant="outline"
disabled={Boolean(busy)}
onClick={() => merge('squash')}
>
{busy === 'squash' ? 'Merging…' : 'Squash and merge'}
</Button>
{error && (
<span className="w-full text-[12.5px] text-destructive">
{error}
</span>
)}
</div>
)}
</div>
</div>
);
}