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.

161 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(viewer: Viewer, query: string, options?: { type?: SearchType | null; page?: number; perPage?: number }): Promise<Result<SearchResults>>;
108 /** A few repositories, work items, pull requests and people, as someone types. */
109 suggest(viewer: Viewer, query: string): Promise<SiteHit[]>;
110 /** Public repositories, recently active or new, by language or topic. */
111 explore(viewer: Viewer, options?: { sort?: "active" | "new"; language?: string | null; topic?: string | null; page?: number }): Promise<Explore>;
112}
113
114export function searchClient(service: ServiceBinding): SearchApi {
115 const call = async <T>(method: string, args: object): Promise<T> => {
116 const response = await service.fetch(`https://service/rpc/${method}`, {
117 method: "POST",
118 headers: { "content-type": "application/json" },
119 body: JSON.stringify(args),
120 });
121 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
122 return (await response.json()) as T;
123 };
124 return {
125 search: (viewer, query, options = {}) =>
126 call("search", { viewer, query, type: options.type ?? null, page: options.page ?? null, perPage: options.perPage ?? null }),
127 suggest: (viewer, query) => call("suggest", { viewer, query }),
128 explore: (viewer, options = {}) =>
129 call("explore", {
130 viewer,
131 sort: options.sort ?? null,
132 language: options.language ?? null,
133 topic: options.topic ?? null,
134 page: options.page ?? null,
135 }),
136 };
137}
138
139/** Reads a type as an address or a person writes it; null when it is not one. */
140export function searchType(value: string | null | undefined): SearchType | null {
141 switch ((value ?? "").trim().toLowerCase()) {
142 case "repositories":
143 case "repos":
144 case "repo":
145 return "repositories";
146 case "code":
147 return "code";
148 case "items":
149 case "item":
150 return "items";
151 case "pulls":
152 case "prs":
153 case "pr":
154 return "pulls";
155 case "people":
156 case "users":
157 return "people";
158 default:
159 return null;
160 }
161}