~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
145 lines · 5.7 KB Raw History
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>
  );
}