Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

80 linesCodeBlameRaw
1import { router, useLocalSearchParams } from "expo-router";
2import * as WebBrowser from "expo-web-browser";
3import { ExternalLink, SearchX } from "lucide-react-native";
4import { useEffect } from "react";
5
6import { Button, Empty, Loading, Screen } from "@/components/ui";
7import { ApiError } from "@/lib/client";
8import { type RefContext, refSitePath, targetRoute } from "@/lib/links";
9import { siteUrl } from "@/lib/server";
10import { useSignedIn } from "@/lib/session";
11import { useRefTarget } from "@/lib/work";
12
13/**
14 * `#N` read in a repository or a workspace: g1t says what it names (a
15 * work item, or a pull request), and that opens in its place. Old links
16 * to a repository's issues come here too, since an old issue's number
17 * still names the work item it became.
18 */
19export default function Ref() {
20 const params = useLocalSearchParams<{ repo?: string; workspace?: string; number: string }>();
21 const { server } = useSignedIn();
22 const number = Number(params.number);
23 const context: RefContext | null = params.repo ? { repo: params.repo } : params.workspace ? { workspace: params.workspace } : null;
24 const target = useRefTarget(context, number);
25 const route = target.data ? targetRoute(target.data) : null;
26
27 const leave = () => (router.canGoBack() ? router.back() : router.replace("/"));
28 const onSite = () => {
29 if (!context) return leave();
30 void WebBrowser.openBrowserAsync(siteUrl(server, refSitePath(context, number))).finally(leave);
31 };
32
33 useEffect(() => {
34 if (route) router.replace(route as never);
35 }, [route]);
36
37 // A g1t that cannot say (or a repository the app cannot read): the site reads the link itself.
38 const unanswered = target.error instanceof ApiError && (target.error.code === "not_found" || target.error.code === "forbidden");
39 useEffect(() => {
40 if (unanswered) onSite();
41 }, [unanswered]);
42
43 if (!context || !Number.isInteger(number)) {
44 return (
45 <Screen safe>
46 <Empty icon={SearchX} title="Not a reference" action={<Button title="Back" variant="secondary" onPress={leave} />} />
47 </Screen>
48 );
49 }
50 if (target.data && !route) {
51 const where = "repo" in context ? context.repo : context.workspace;
52 return (
53 <Screen safe>
54 <Empty
55 icon={SearchX}
56 title={`Nothing is #${number} in ${where}`}
57 body="It may have been deleted, or be somewhere you cannot see."
58 action={<Button title="Back" variant="secondary" onPress={leave} />}
59 />
60 </Screen>
61 );
62 }
63 if (target.error && !unanswered) {
64 return (
65 <Screen safe>
66 <Empty
67 icon={SearchX}
68 title={`Could not find #${number}`}
69 body={target.error.message}
70 action={<Button title="Open on g1t" variant="secondary" icon={ExternalLink} onPress={onSite} />}
71 />
72 </Screen>
73 );
74 }
75 return (
76 <Screen safe>
77 <Loading />
78 </Screen>
79 );
80}