flagon-io/g1t

public

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

g1t/packages/contracts/src/search.ts

159 lines5,034 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Search across all of g1t, Explore, and a command palette1import 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, issues 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" | "issues" | "pulls" | "people";
14
15export const SEARCH_TYPES: SearchType[] = ["repositories", "code", "issues", "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" | "issue" | "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, an issue'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. */
39 repo: string | null;
40 private: boolean;
41 description: string | null;
42 snippet: Segment[];
43 /** Code only: the lines that matched, with a line around each. */
44 lines: CodeLine[];
45 path: string | null;
46 language: string | null;
47 ref: string | null;
48 number: number | null;
49 /** `open`/`closed` for an issue; `draft`/`open`/`merged`/`closed` for a pull request. */
50 state: string | null;
51 author: string | null;
52 labels: string[];
53 topics: string[];
54 /** A username or a workspace's slug. */
55 slug: string | null;
56 avatar: string | null;
57 /** RFC 3339. */
58 updatedAt: string | null;
59};
60
61export type SearchCounts = Record<SearchType, number>;
62
63export type SearchResults = {
64 /** The query as it was read. */
65 query: string;
66 type: SearchType;
67 counts: SearchCounts;
68 page: number;
69 perPage: number;
70 more: boolean;
71 hits: SiteHit[];
72 /** What the query could not do, said plainly. */
73 notes: string[];
74};
75
76export type ExploreRepo = {
77 namespace: string;
78 name: string;
79 description: string | null;
80 topics: string[];
81 language: string | null;
82 createdAt: string;
83 pushedAt: string | null;
84};
85
86export type Facet = { name: string; count: number };
87
88export type Explore = {
89 repos: ExploreRepo[];
90 languages: Facet[];
91 topics: Facet[];
92 page: number;
93 more: boolean;
94};
95
96export interface SearchApi {
97 /** One page of results of one type, with counts for every type. */
98 search(
99 viewer: Viewer,
100 query: string,
101 options?: { type?: SearchType | null; page?: number; perPage?: number },
102 ): Promise<Result<SearchResults>>;
103 /** A few repositories, issues, pull requests and people, as someone types. */
104 suggest(viewer: Viewer, query: string): Promise<SiteHit[]>;
105 /** Public repositories, recently active or new, by language or topic. */
106 explore(
107 viewer: Viewer,
108 options?: { sort?: "active" | "new"; language?: string | null; topic?: string | null; page?: number },
109 ): Promise<Explore>;
110}
111
112export function searchClient(service: ServiceBinding): SearchApi {
113 const call = async <T>(method: string, args: object): Promise<T> => {
114 const response = await service.fetch(`https://service/rpc/${method}`, {
115 method: "POST",
116 headers: { "content-type": "application/json" },
117 body: JSON.stringify(args),
118 });
119 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
120 return (await response.json()) as T;
121 };
122 return {
123 search: (viewer, query, options = {}) =>
124 call("search", { viewer, query, type: options.type ?? null, page: options.page ?? null, perPage: options.perPage ?? null }),
125 suggest: (viewer, query) => call("suggest", { viewer, query }),
126 explore: (viewer, options = {}) =>
127 call("explore", {
128 viewer,
129 sort: options.sort ?? null,
130 language: options.language ?? null,
131 topic: options.topic ?? null,
132 page: options.page ?? null,
133 }),
134 };
135}
136
137/** Reads a type as an address or a person writes it; null when it is not one. */
138export function searchType(value: string | null | undefined): SearchType | null {
139 switch ((value ?? "").trim().toLowerCase()) {
140 case "repositories":
141 case "repos":
142 case "repo":
143 return "repositories";
144 case "code":
145 return "code";
146 case "issues":
147 case "issue":
148 return "issues";
149 case "pulls":
150 case "prs":
151 case "pr":
152 return "pulls";
153 case "people":
154 case "users":
155 return "people";
156 default:
157 return null;
158 }
159}