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.

360 linesCodeBlameRaw
1//! The search service: one search across all of g1t, and Explore.
2//!
3//! It covers everything public, and private content the viewer is a member
4//! of: repositories (name, description, topics, README), code on default
5//! branches, work items and pull requests, people and workspaces. Who may see
6//! what is decided when the query runs, against the viewer's current
7//! memberships and each repository's current visibility, never only when
8//! something was indexed.
9//!
10//! Each `*Args` struct is the argument of the method of the same name,
11//! served at `POST /rpc/<method>`. Mirrors `packages/contracts/src/search.ts`.
12
13use serde::{Deserialize, Serialize};
14
15use crate::Viewer;
16
17/// What a search looks through: one tab of results.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum SearchType {
21 Repositories,
22 Code,
23 Items,
24 Pulls,
25 People,
26}
27
28impl SearchType {
29 pub const ALL: [SearchType; 5] = [
30 SearchType::Repositories,
31 SearchType::Code,
32 SearchType::Items,
33 SearchType::Pulls,
34 SearchType::People,
35 ];
36
37 pub fn as_str(self) -> &'static str {
38 match self {
39 SearchType::Repositories => "repositories",
40 SearchType::Code => "code",
41 SearchType::Items => "items",
42 SearchType::Pulls => "pulls",
43 SearchType::People => "people",
44 }
45 }
46
47 /// Reads a type as people write it: `repositories`, `repos`, `code`,
48 /// `items`, `pulls`, `prs`, `people`, `users`.
49 pub fn parse(text: &str) -> Option<SearchType> {
50 match text.trim().to_lowercase().as_str() {
51 "repositories" | "repository" | "repos" | "repo" | "projects" | "project" => Some(SearchType::Repositories),
52 "code" => Some(SearchType::Code),
53 "items" | "item" => Some(SearchType::Items),
54 "pulls" | "pull" | "prs" | "pr" | "pull_requests" | "pull-requests" => Some(SearchType::Pulls),
55 "people" | "users" | "user" | "workspaces" | "workspace" => Some(SearchType::People),
56 _ => None,
57 }
58 }
59}
60
61/// The most results on one page.
62pub const MAX_PER_PAGE: u32 = 50;
63/// Results on a page when none is asked for.
64pub const DEFAULT_PER_PAGE: u32 = 20;
65/// Counts stop here: past it a tab says "1,000+".
66pub const COUNT_CAP: u32 = 1000;
67/// The longest query read; the rest is ignored.
68pub const MAX_QUERY_CHARS: usize = 256;
69
70/// `search`. Returns `Outcome<SearchResults>`.
71#[derive(Debug, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct SearchArgs {
74 pub viewer: Viewer,
75 /// Words, quoted phrases and qualifiers, such as
76 /// `parse_query repo:acme/web language:rust`.
77 pub query: String,
78 /// Which tab of results. Worked out from the qualifiers when absent:
79 /// `path:` means code, `is:pr` pull requests, and so on.
80 #[serde(default, rename = "type")]
81 pub kind: Option<SearchType>,
82 /// From 1.
83 #[serde(default)]
84 pub page: Option<u32>,
85 #[serde(default)]
86 pub per_page: Option<u32>,
87}
88
89/// One piece of a snippet, highlighted where it matched the query.
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
91pub struct Segment {
92 pub text: String,
93 #[serde(default)]
94 pub highlight: bool,
95}
96
97/// A line of code in a result, numbered from 1.
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99pub struct CodeLine {
100 pub number: u32,
101 pub parts: Vec<Segment>,
102}
103
104/// What a result is.
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "lowercase")]
107pub enum HitKind {
108 Repository,
109 Code,
110 Item,
111 Pull,
112 User,
113 Workspace,
114}
115
116/// One result. Which fields are set depends on `kind`.
117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct Hit {
120 pub kind: HitKind,
121 /// What to show as the result's name: `acme/web`, a file's path,
122 /// `Fix the login redirect`, a person's name.
123 pub title: String,
124 /// Where it is on g1t.sh, as a path: `/acme/web/blob/main/src/app.rs#L12`.
125 pub url: String,
126 /// The repository, as `owner/name`, for everything but people and
127 /// workspaces: for a work item, the one it was filed in, if any.
128 #[serde(default)]
129 pub repo: Option<String>,
130 /// For a work item: its workspace's slug.
131 #[serde(default)]
132 pub workspace: Option<String>,
133 /// Whether only members of its workspace can see it.
134 #[serde(default)]
135 pub private: bool,
136 /// A repository's or workspace's description, a person's bio.
137 #[serde(default)]
138 pub description: Option<String>,
139 /// The part of the text that matched, highlighted. Empty for code.
140 #[serde(default)]
141 pub snippet: Vec<Segment>,
142 /// For code: the lines that matched, with a line around each.
143 #[serde(default)]
144 pub lines: Vec<CodeLine>,
145 /// For code: the file's path in its repository.
146 #[serde(default)]
147 pub path: Option<String>,
148 /// For code: the file's language. For a repository: the language most
149 /// of its code is in.
150 #[serde(default)]
151 pub language: Option<String>,
152 /// For code: the branch it was read from.
153 #[serde(default, rename = "ref")]
154 pub git_ref: Option<String>,
155 /// For work items and pull requests.
156 #[serde(default)]
157 pub number: Option<u32>,
158 /// `open` or `closed` for a work item; `draft`, `open`, `merged` or
159 /// `closed` for a pull request.
160 #[serde(default)]
161 pub state: Option<String>,
162 /// Who opened the work item or pull request: `g1t` for one g1t made.
163 #[serde(default)]
164 pub author: Option<String>,
165 /// For a work item or pull request g1t opened: who asked for it.
166 #[serde(default)]
167 pub requested_by: Option<String>,
168 #[serde(default)]
169 pub labels: Vec<String>,
170 /// A repository's topics.
171 #[serde(default)]
172 pub topics: Vec<String>,
173 /// A user's username or a workspace's slug.
174 #[serde(default)]
175 pub slug: Option<String>,
176 /// The uploaded avatar's hash, for people and workspaces.
177 #[serde(default)]
178 pub avatar: Option<String>,
179 /// RFC 3339: when it last changed, or a repository's last push.
180 #[serde(default)]
181 pub updated_at: Option<String>,
182}
183
184impl Hit {
185 pub fn new(kind: HitKind, title: impl Into<String>, url: impl Into<String>) -> Hit {
186 Hit {
187 kind,
188 title: title.into(),
189 url: url.into(),
190 repo: None,
191 workspace: None,
192 private: false,
193 description: None,
194 snippet: Vec::new(),
195 lines: Vec::new(),
196 path: None,
197 language: None,
198 git_ref: None,
199 number: None,
200 state: None,
201 author: None,
202 requested_by: None,
203 labels: Vec::new(),
204 topics: Vec::new(),
205 slug: None,
206 avatar: None,
207 updated_at: None,
208 }
209 }
210}
211
212/// How many results each tab has, up to [`COUNT_CAP`].
213#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
214pub struct SearchCounts {
215 pub repositories: u32,
216 pub code: u32,
217 pub items: u32,
218 pub pulls: u32,
219 pub people: u32,
220}
221
222impl SearchCounts {
223 pub fn get(&self, kind: SearchType) -> u32 {
224 match kind {
225 SearchType::Repositories => self.repositories,
226 SearchType::Code => self.code,
227 SearchType::Items => self.items,
228 SearchType::Pulls => self.pulls,
229 SearchType::People => self.people,
230 }
231 }
232
233 pub fn set(&mut self, kind: SearchType, count: u32) {
234 let slot = match kind {
235 SearchType::Repositories => &mut self.repositories,
236 SearchType::Code => &mut self.code,
237 SearchType::Items => &mut self.items,
238 SearchType::Pulls => &mut self.pulls,
239 SearchType::People => &mut self.people,
240 };
241 *slot = count;
242 }
243}
244
245#[derive(Clone, Debug, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct SearchResults {
248 /// The query as it was read.
249 pub query: String,
250 /// The tab these results are for.
251 #[serde(rename = "type")]
252 pub kind: SearchType,
253 pub counts: SearchCounts,
254 pub page: u32,
255 pub per_page: u32,
256 /// Whether there is a page after this one.
257 pub more: bool,
258 pub hits: Vec<Hit>,
259 /// What the query could not do, such as a code search with no word of
260 /// three characters or more.
261 #[serde(default)]
262 pub notes: Vec<String>,
263}
264
265/// `suggest`: a few results of every kind but code, for the command
266/// palette as someone types. Returns `Vec<Hit>`.
267#[derive(Debug, Default, Serialize, Deserialize)]
268pub struct SuggestArgs {
269 pub viewer: Viewer,
270 pub query: String,
271}
272
273/// `explore`: public repositories, recently active or newly created,
274/// optionally of one language or topic. Returns `Explore`.
275#[derive(Debug, Default, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct ExploreArgs {
278 pub viewer: Viewer,
279 /// `active` (the default) or `new`.
280 #[serde(default)]
281 pub sort: Option<String>,
282 #[serde(default)]
283 pub language: Option<String>,
284 #[serde(default)]
285 pub topic: Option<String>,
286 #[serde(default)]
287 pub page: Option<u32>,
288}
289
290/// A public repository as Explore lists it.
291#[derive(Clone, Debug, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct ExploreRepo {
294 /// The repository's id, for what other services keep about it, such as
295 /// its project's description.
296 #[serde(default)]
297 pub id: String,
298 pub namespace: String,
299 pub name: String,
300 pub description: Option<String>,
301 #[serde(default)]
302 pub topics: Vec<String>,
303 pub language: Option<String>,
304 /// RFC 3339.
305 pub created_at: String,
306 /// RFC 3339: the last push to its default branch, if any.
307 pub pushed_at: Option<String>,
308 /// Whether it is archived: read-only, kept for reference.
309 #[serde(default)]
310 pub archived: bool,
311}
312
313/// A language or topic, with how many public repositories have it.
314#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
315pub struct Facet {
316 pub name: String,
317 pub count: u32,
318}
319
320#[derive(Clone, Debug, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct Explore {
323 pub repos: Vec<ExploreRepo>,
324 pub languages: Vec<Facet>,
325 pub topics: Vec<Facet>,
326 pub page: u32,
327 pub more: bool,
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn types_read_as_people_write_them() {
336 assert_eq!(SearchType::parse("prs"), Some(SearchType::Pulls));
337 assert_eq!(SearchType::parse("Repos"), Some(SearchType::Repositories));
338 assert_eq!(SearchType::parse("users"), Some(SearchType::People));
339 assert_eq!(SearchType::parse("items"), Some(SearchType::Items));
340 assert_eq!(SearchType::parse("issues"), None);
341 assert_eq!(SearchType::parse("wiki"), None);
342 }
343
344 #[test]
345 fn the_type_is_sent_as_type() {
346 let results = SearchResults {
347 query: "x".into(),
348 kind: SearchType::Code,
349 counts: SearchCounts::default(),
350 page: 1,
351 per_page: 20,
352 more: false,
353 hits: vec![],
354 notes: vec![],
355 };
356 let value = serde_json::to_value(&results).unwrap();
357 assert_eq!(value["type"], "code");
358 assert_eq!(value["perPage"], 20);
359 }
360}