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.

168 linesCodeBlameRaw
1import type { ServiceBinding } from "./clients";
2import type { Viewer } from "./identity";
3import type { Result } from "./result";
4
5/**
6 * Search across all of g1t: repositories (name, description, topics,
7 * README), code on default branches, work items and pull requests, people and
8 * workspaces. Everything public, and private content the viewer is a member
9 * of, decided when the query runs. Mirrors `crates/contracts/src/search.rs`.
10 */
11
12/** One tab of results. */
13export type SearchType = "repositories" | "code" | "items" | "pulls" | "people";
14
15export const SEARCH_TYPES: SearchType[] = ["repositories", "code", "items", "pulls", "people"];
16
17/** The most results on one page, and how many a page has when none is asked for. */
18export const MAX_PER_PAGE = 50;
19export const DEFAULT_PER_PAGE = 20;
20/** Counts stop here: past it a tab says "1,000+". */
21export const COUNT_CAP = 1000;
22
23/** A piece of a snippet, highlighted where it matched the query. */
24export type Segment = { text: string; highlight: boolean };
25
26/** A line of code in a result, numbered from 1. */
27export type CodeLine = { number: number; parts: Segment[] };
28
29export type HitKind = "repository" | "code" | "item" | "pull" | "user" | "workspace";
30
31/** One result. Which fields are set depends on `kind`. */
32export type SiteHit = {
33 kind: HitKind;
34 /** `acme/web`, a file's path, a work item's title, a person's name. */
35 title: string;
36 /** A path on g1t.sh: `/acme/web/blob/main/src/app.rs#L12`. */
37 url: string;
38 /** `owner/name`, for everything but people: for a work item, the one it was filed in, if any. */
39 repo: string | null;
40 /** For a work item: its workspace's slug. */
41 workspace?: string | null;
42 private: boolean;
43 description: string | null;
44 snippet: Segment[];
45 /** Code only: the lines that matched, with a line around each. */
46 lines: CodeLine[];
47 path: string | null;
48 language: string | null;
49 ref: string | null;
50 number: number | null;
51 /** `open`/`closed` for a work item; `draft`/`open`/`merged`/`closed` for a pull request. */
52 state: string | null;
53 /** Who opened the work item or pull request: `g1t` for one g1t made. */
54 author: string | null;
55 /** For a work item or pull request g1t opened: who asked for it. */
56 requestedBy?: string | null;
57 labels: string[];
58 topics: string[];
59 /** A username or a workspace's slug. */
60 slug: string | null;
61 avatar: string | null;
62 /** RFC 3339. */
63 updatedAt: string | null;
64};
65
66export type SearchCounts = Record<SearchType, number>;
67
68export type SearchResults = {
69 /** The query as it was read. */
70 query: string;
71 type: SearchType;
72 counts: SearchCounts;
73 page: number;
74 perPage: number;
75 more: boolean;
76 hits: SiteHit[];
77 /** What the query could not do, said plainly. */
78 notes: string[];
79};
80
81export type ExploreRepo = {
82 /** The repository's id: its description is its project's, read by it. */
83 id: string;
84 namespace: string;
85 name: string;
86 description: string | null;
87 topics: string[];
88 language: string | null;
89 createdAt: string;
90 pushedAt: string | null;
91 /** Whether it is archived: read-only, kept for reference. */
92 archived: boolean;
93};
94
95export type Facet = { name: string; count: number };
96
97export type Explore = {
98 repos: ExploreRepo[];
99 languages: Facet[];
100 topics: Facet[];
101 page: number;
102 more: boolean;
103};
104
105export interface SearchApi {
106 /** One page of results of one type, with counts for every type. */
107 search(
108 viewer: Viewer,
109 query: string,
110 options?: { type?: SearchType | null; page?: number; perPage?: number },
111 ): Promise<Result<SearchResults>>;
112 /** A few repositories, work items, pull requests and people, as someone types. */
113 suggest(viewer: Viewer, query: string): Promise<SiteHit[]>;
114 /** Public repositories, recently active or new, by language or topic. */
115 explore(
116 viewer: Viewer,
117 options?: { sort?: "active" | "new"; language?: string | null; topic?: string | null; page?: number },
118 ): Promise<Explore>;
119}
120
121export function searchClient(service: ServiceBinding): SearchApi {
122 const call = async <T>(method: string, args: object): Promise<T> => {
123 const response = await service.fetch(`https://service/rpc/${method}`, {
124 method: "POST",
125 headers: { "content-type": "application/json" },
126 body: JSON.stringify(args),
127 });
128 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
129 return (await response.json()) as T;
130 };
131 return {
132 search: (viewer, query, options = {}) =>
133 call("search", { viewer, query, type: options.type ?? null, page: options.page ?? null, perPage: options.perPage ?? null }),
134 suggest: (viewer, query) => call("suggest", { viewer, query }),
135 explore: (viewer, options = {}) =>
136 call("explore", {
137 viewer,
138 sort: options.sort ?? null,
139 language: options.language ?? null,
140 topic: options.topic ?? null,
141 page: options.page ?? null,
142 }),
143 };
144}
145
146/** Reads a type as an address or a person writes it; null when it is not one. */
147export function searchType(value: string | null | undefined): SearchType | null {
148 switch ((value ?? "").trim().toLowerCase()) {
149 case "repositories":
150 case "repos":
151 case "repo":
152 return "repositories";
153 case "code":
154 return "code";
155 case "items":
156 case "item":
157 return "items";
158 case "pulls":
159 case "prs":
160 case "pr":
161 return "pulls";
162 case "people":
163 case "users":
164 return "people";
165 default:
166 return null;
167 }
168}