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.

Commit

Search across all of g1t, Explore, and a command palette

- services/search: one D1 FTS5 index of repositories (with topics and README), code on default branches (trigram), issues and pull requests, and people and workspaces. Fed by events: a push re-indexes only the files it changed, in capped jobs; a backfill starts on its own. - Visibility is checked when the query runs, against current membership and again against the repos service, so a project made private leaves others' results at once. Signed-out visitors search everything public. - Qualifiers: repo:, workspace:, language:, path:, is:, author:, label:, and quoted phrases. GET /search and the MCP search tool, for people and agents. - /search, a search box in the top bar, a Ctrl-K palette with instant results, per-project search from Code, and Explore for public projects by activity, language and topic. Repositories gain topics. - A workflow can be run by its id as well as by its file.

syntaqxcommitted Parent2fe4615Browse files
66 files+739−110/66 viewed
+11−0
10251025 ]
10261026
10271027 [[package]]
1028+name = "g1t-search"
1029+version = "0.1.0"
1030+dependencies = [
1031+ "g1t-contracts",
1032+ "g1t-kit",
1033+ "serde",
1034+ "serde_json",
1035+ "worker",
1036+]
1037+
1038+[[package]]
10281039 name = "g1t-secrets"
10291040 version = "0.1.0"
10301041 dependencies = [
+1−1
11 [workspace]
22 resolver = "3"
3−members = ["apps/api", "crates/*", "services/actions", "services/billing", "services/events", "services/identity", "services/integrations", "services/webhooks", "services/repos", "services/security", "services/work"]
3+members = ["apps/api", "crates/*", "services/actions", "services/billing", "services/events", "services/identity", "services/integrations", "services/webhooks", "services/repos", "services/search", "services/security", "services/work"]
44
55 [workspace.package]
66 edition = "2024"
+1−0
122122 "current_user_url": format!("{API}/user"),
123123 "workspaces_url": format!("{API}/workspaces"),
124124 "repositories_url": format!("{API}/repos{{?q}}"),
125+ "search_url": format!("{API}/search{{?q,type,page,per_page}}"),
125126 "repository_url": repo,
126127 "repository_events_url": format!("{repo}/events{{?before}}"),
127128 "labels_url": format!("{repo}/labels"),
+7−1
8484 &[Op::Remember, Op::Recall],
8585 ),
8686 (
87+ "Search",
88+ "One search across all of g1t: repositories, code, issues, pull requests, people and workspaces. Public content for everyone, and private content in workspaces you belong to.",
89+ &[Op::Search],
90+ ),
91+ (
8792 "Context",
8893 "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.",
8994 &[Op::SearchContext, Op::GetEntity],
170175 Op::Recall => "Recall memory",
171176 Op::SearchContext => "Search the context hub",
172177 Op::GetEntity => "Get a catalog entry",
178+ Op::Search => "Search g1t",
173179 Op::ListIssues => "List issues",
174180 Op::GetIssue => "Get an issue",
175181 Op::CreateIssue => "Create an issue",
383389 );
384390 }
385391 responses.insert("403".into(), error_response("Signed in, but not allowed to do this."));
386− if !matches!(op, Op::Whoami | Op::ListRepos) {
392+ if !matches!(op, Op::Whoami | Op::ListRepos | Op::Search) {
387393 responses.insert("404".into(), error_response("It does not exist, or you cannot see it."));
388394 }
389395 if route.method != "GET" {
+48−2
2828 pub actions: Fetcher,
2929 /// The context hub: catalog and search.
3030 pub context: Fetcher,
31+ /// Search across all of g1t.
32+ pub search: Fetcher,
3133 /// Where the request came in, for its audit entries.
3234 pub audit: crate::audit::AuditContext,
3335 /// Set for a request made with an agent's token: all it may do.
4749 webhooks: env.service("WEBHOOKS")?,
4850 actions: env.service("ACTIONS")?,
4951 context: env.service("CONTEXT")?,
52+ search: env.service("SEARCH")?,
5053 scope: None,
5154 audit: crate::audit::AuditContext::default(),
5255 })
7174 Recall,
7275 SearchContext,
7376 GetEntity,
77+ Search,
7478 ListIssues,
7579 GetIssue,
7680 CreateIssue,
295299 }
296300
297301 impl Op {
298− pub const ALL: [Op; 68] = [
302+ pub const ALL: [Op; 69] = [
299303 Op::Whoami,
300304 Op::CreateWorkspace,
301305 Op::ListRepos,
312316 Op::Recall,
313317 Op::SearchContext,
314318 Op::GetEntity,
319+ Op::Search,
315320 Op::ListIssues,
316321 Op::GetIssue,
317322 Op::CreateIssue,
388393 Op::Recall => "recall",
389394 Op::SearchContext => "search_context",
390395 Op::GetEntity => "get_entity",
396+ Op::Search => "search",
391397 Op::UpdateRepoSettings => "update_repo_settings",
392398 Op::ListIssues => "list_issues",
393399 Op::GetIssue => "get_issue",
455461 Op::ListRepos => "Repositories you can see, optionally filtered by a search query.",
456462 Op::GetRepo => "One repository's details.",
457463 Op::UpdateRepo => {
458− "Change a repository's description, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only."
464+ "Change a repository's description, its topics, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only."
459465 }
460466 Op::GetRepoSettings => {
461467 "How a repository handles pull requests: the approvals a merge needs, whether failed checks can be overridden, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged."
478484 Op::SearchContext => {
479485 "One search across a workspace's context hub: its catalog (projects, apps, APIs, packages, languages, owners, environments, integrations, docs), the text of its docs, its issues and pull requests, and, for members and g1t's agents, its kept memory. Results are ranked by meaning, each labelled with its kind, where it came from, who wrote it and how fresh it is; matching words answers when meaning cannot. Give the workspace, or a repository in it. Narrow with project (a project's slug) and kinds. Reads only what you may see: memory and private projects are for members."
480486 }
487+ Op::Search => {
488+ "Search all of g1t: repositories (name, description, topics, README), code on default branches (file names and contents), issues, pull requests, people and workspaces. Covers everything public, and private content in workspaces you belong to; signed out, public only. Write words, \"exact phrases\", -words to leave out, and qualifiers: repo:owner/name, org:workspace, language:rust, path:src/ (a glob with *), is:issue, is:pr, is:open, is:closed, is:merged, author:username, label:bug. type picks the kind of results (repositories, code, issues, pulls or people); without it, the qualifiers decide. Returns one page of results with the matches highlighted, code with line numbers, and how many there are of each kind."
489+ }
481490 Op::GetEntity => {
482491 "One entry of a workspace's catalog, by kind and its id or key (a project's slug, a package as npm:<name>, an owner's username), with every relation it has: what it depends on, who owns it, where it deploys, what documents it, what it exposes and uses. search_context finds entries."
483492 }
656665 "type": "boolean",
657666 "description": "Refuse pushes to the default branch, so that it changes only by merging a pull request.",
658667 },
668+ "topics": {
669+ "type": "array",
670+ "items": { "type": "string" },
671+ "description": "Replaces its topics, which search and Explore show: lowercase letters, digits and hyphens, at most 20. An empty list clears them.",
672+ },
659673 }),
660674 &["repo"],
661675 ),
733747 }),
734748 &["query"],
735749 ),
750+ Op::Search => object(
751+ json!({
752+ "query": { "type": "string", "description": "What to look for: words, \"phrases\" and qualifiers, such as parse_query language:rust repo:acme/web." },
753+ "type": {
754+ "type": "string",
755+ "enum": ["repositories", "code", "issues", "pulls", "people"],
756+ "description": "Which kind of results. Worked out from the qualifiers if not given: path: means code, is:pr pull requests, is:open or label: issues, otherwise repositories.",
757+ },
758+ "page": { "type": "integer", "description": "From 1; at most 50." },
759+ "per_page": { "type": "integer", "description": "At most 50; 20 if not given." },
760+ }),
761+ &["query"],
762+ ),
736763 Op::GetEntity => object(
737764 json!({
738765 "kind": {
11841211 !matches!(
11851212 self,
11861213 Op::ListRepos
1214+ | Op::Search
11871215 | Op::GetRepo
11881216 | Op::ListIssues
11891217 | Op::GetIssue
12111239 | Op::CreateWorkspace
12121240 | Op::SearchContext
12131241 | Op::GetEntity
1242+ | Op::Search
12141243 | Op::ListRepos
12151244 | Op::CreateRepo
12161245 | Op::ListIntegrations
13821411 "description": input["description"].as_str(),
13831412 "isPrivate": input["private"].as_bool(),
13841413 "protected": input["protected"].as_bool(),
1414+ "topics": strings(input, "topics"),
13851415 }),
13861416 )
13871417 .await
14951525 .await
14961526 }
14971527 }
1528+ Op::Search => {
1529+ pass(
1530+ &services.search,
1531+ "search",
1532+ &json!({
1533+ "viewer": viewer,
1534+ "query": text(input, "query"),
1535+ "type": optional_text(input, "type").and_then(|kind| {
1536+ g1t_contracts::search::SearchType::parse(&kind).map(|kind| kind.as_str())
1537+ }),
1538+ "page": integer(input, "page"),
1539+ "perPage": integer(input, "per_page"),
1540+ }),
1541+ )
1542+ .await
1543+ }
14981544 Op::Recall => {
14991545 pass(
15001546 work,
+81−0
26252625 },
26262626 "notes": "Members of the workspace only. Each memory returned is marked used, which keeps it near the front of what agents are given."
26272627 },
2628+ "search": {
2629+ "query": {
2630+ "q": "parse_query language:rust repo:acme/web",
2631+ "type": "code"
2632+ },
2633+ "response": {
2634+ "query": "parse_query repo:acme/web language:rust",
2635+ "type": "code",
2636+ "counts": {
2637+ "repositories": 0,
2638+ "code": 2,
2639+ "issues": 0,
2640+ "pulls": 0,
2641+ "people": 0
2642+ },
2643+ "page": 1,
2644+ "per_page": 20,
2645+ "more": false,
2646+ "hits": [
2647+ {
2648+ "kind": "code",
2649+ "title": "src/search/query.rs",
2650+ "url": "/acme/web/blob/main/src/search/query.rs#L42",
2651+ "repo": "acme/web",
2652+ "private": false,
2653+ "description": null,
2654+ "snippet": [],
2655+ "lines": [
2656+ {
2657+ "number": 41,
2658+ "parts": [
2659+ {
2660+ "text": "/// Reads a query typed into the search box.",
2661+ "highlight": false
2662+ }
2663+ ]
2664+ },
2665+ {
2666+ "number": 42,
2667+ "parts": [
2668+ {
2669+ "text": "pub fn ",
2670+ "highlight": false
2671+ },
2672+ {
2673+ "text": "parse_query",
2674+ "highlight": true
2675+ },
2676+ {
2677+ "text": "(text: &str) -> Query {",
2678+ "highlight": false
2679+ }
2680+ ]
2681+ },
2682+ {
2683+ "number": 43,
2684+ "parts": [
2685+ {
2686+ "text": " let mut query = Query::default();",
2687+ "highlight": false
2688+ }
2689+ ]
2690+ }
2691+ ],
2692+ "path": "src/search/query.rs",
2693+ "language": "rust",
2694+ "ref": "main",
2695+ "number": null,
2696+ "state": null,
2697+ "author": null,
2698+ "labels": [],
2699+ "topics": [],
2700+ "slug": null,
2701+ "avatar": null,
2702+ "updated_at": null
2703+ }
2704+ ],
2705+ "notes": []
2706+ },
2707+ "notes": "`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to members of its workspace, checked when the search runs, so a repository made private stops appearing at once. A g1t agent's token can search too."
2708+ },
26282709 "search_context": {
26292710 "params": {
26302711 "workspace": "acme"
+2−1
77 //! encoded again, so that every field the type has is sent, not only the
88 //! ones an example shows.
99
10−use g1t_contracts::{actions, integrations, repos, webhooks, work};
10+use g1t_contracts::{actions, integrations, repos, search, webhooks, work};
1111 use g1t_kit::wire::{self, USER_KEYED};
1212 use serde::Serialize;
1313 use serde::de::DeserializeOwned;
6666 let sent = as_services_send(example);
6767 match op {
6868 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
69+ Op::Search => through::<search::SearchResults>(op, sent),
6970 Op::GetRepo | Op::CreateRepo | Op::UpdateRepo => through::<repos::Repo>(op, sent),
7071 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
7172 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
+6−0
3131 route("GET", "/user", Op::Whoami, &[]),
3232 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
3333 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
34+ route(
35+ "GET",
36+ "/search",
37+ Op::Search,
38+ &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
39+ ),
3440 route("POST", "/repos", Op::CreateRepo, &[]),
3541 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
3642 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
+2−1
2525 // Builds of deployments report through here.
2626 { "binding": "DEPLOYMENTS", "service": "g1t-deployments" },
2727 // The context hub: search_context and get_entity.
28− { "binding": "CONTEXT", "service": "g1t-context" }
28+ { "binding": "CONTEXT", "service": "g1t-context" },
29+ { "binding": "SEARCH", "service": "g1t-search" }
2930 ],
3031 // GitHub Actions artifacts and cache, in chunks, with KV's own expiry.
3132 // (Moves to R2 once R2 is enabled on the account.)
+1−0
7373 items: [
7474 { label: 'Quickstart', slug: 'quickstart' },
7575 { label: 'How g1t works', slug: 'concepts/overview' },
76+ { label: 'Search and Explore', slug: 'guides/search' },
7677 ],
7778 },
7879 {
+1−1
367367
368368 | Kind of run | Git | API and MCP tools |
369369 | --- | --- | --- |
370−| Implement | Reads the repository; pushes to its pull request's fork only | Records the session and marks its own pull request ready; tools to read issues, pull requests, the merge queue, workflow runs and memory, open issues, comment, remember, and message other agents |
370+| Implement | Reads the repository; pushes to its pull request's fork only | Records the session and marks its own pull request ready; tools to read issues, pull requests, the merge queue, workflow runs and memory, to [search all of g1t](/guides/search/) and the workspace's context hub, open issues, comment, remember, and message other agents |
371371 | Revise, answer | Reads the repository; pushes to the pull request's fork, or to its branch only when the change is a branch of the repository | Records the session of its own pull request; the same tools as implement |
372372 | Catch up | Reads the repository; pushes to the pull request's fork or branch only | Records the session of its own pull request |
373373 | Review | Reads the change and the repository; pushes nothing | Reports its review through its own run |
+1−1
5252 | **Deployments** | Production, previews, build command, output directory and idle days. See [Deployments](/guides/deployments/#settings). |
5353 | **Dependencies** | The projects this one uses, and the ones that use it. See [Dependencies](#dependencies). |
5454 | **Secrets and variables** | The project's rows. See [Secrets and variables](/guides/secrets-and-variables/). |
55−| **Repository** | The repository's visibility, branch protection, required approvals, checks, the merge queue and auto-merge. |
55+| **Repository** | The repository's description, [topics](/guides/search/#what-is-indexed), visibility, branch protection, required approvals, checks, the merge queue and auto-merge. |
5656 | **Webhooks** | The repository's [webhooks](/guides/webhooks/). |
5757
5858 The **root directory** says where in the repository the project lives,
+170−0
1+---
2+title: Search and Explore
3+description: Search all of g1t at once (repositories, code, issues, pull requests, people and workspaces) with qualifiers, and browse public projects on Explore.
4+---
5+
6+**Search** looks through all of g1t at once: repositories, the code on
7+their default branches, issues, pull requests, people and workspaces. It
8+covers everything public, and everything private in the workspaces you
9+belong to. Signed out, you see public results only.
10+
11+**Explore**, at [g1t.sh/explore](https://g1t.sh/explore), lists public
12+projects: the recently active ones, the new ones, and the ones in a
13+language or about a topic.
14+
15+## Search from anywhere
16+
17+There are three ways in:
18+
19+| Where | What it does |
20+| --- | --- |
21+| The search box in the top bar | Opens [g1t.sh/search](https://g1t.sh/search) with what you typed |
22+| **⌘K** (Ctrl-K on Windows and Linux) | Opens the command palette. As you type it shows matching repositories, issues, pull requests and people, the pages you can go to, and rows that search all of g1t or only code for what you typed |
23+| A project's **Code** page | **Search this repository's code** searches the project's repository, with `repo:` filled in |
24+
25+The search page has a tab for each kind of result, each with its count:
26+
27+| Tab | Searches |
28+| --- | --- |
29+| **Repositories** | Each repository's name, workspace, description, topics and the opening of its README |
30+| **Code** | The file names and contents of every repository's default branch |
31+| **Issues** | Issue titles and descriptions |
32+| **Pull requests** | Pull request titles and descriptions |
33+| **People** | Usernames and names of people, and the slugs, names and descriptions of workspaces |
34+
35+Results show the part that matched, highlighted. A code result shows the
36+lines that matched with their line numbers, and each line links to that
37+line of the file, at `/<owner>/<repo>/blob/<branch>/<path>#L<line>`. Counts
38+stop at 1,000. Results come 20 to a page.
39+
40+## Writing a query
41+
42+Words match wherever they appear, in any order. A result has every word
43+you type.
44+
45+| You type | It finds |
46+| --- | --- |
47+| `merge queue` | Results with both words, anywhere |
48+| `"merge queue"` | The two words together, in this order |
49+| `parse -legacy` | Results with `parse` and without `legacy` |
50+| `pars` | In repositories, issues, pull requests and people, any word that starts with `pars`, such as `parser` |
51+| `parse_query(` | In code, that exact run of characters, punctuation and all |
52+
53+Code is matched as text, the way you would find something in an editor:
54+any run of three characters or more, inside words or across them. A code
55+search needs at least one word of three characters or more, unless it is
56+limited to repositories with `repo:`.
57+
58+### Qualifiers
59+
60+Put qualifiers anywhere in the query. Most can be left out with a leading
61+`-`, such as `-label:wontfix`.
62+
63+| Qualifier | Narrows to | Applies to |
64+| --- | --- | --- |
65+| `repo:owner/name` | One repository. Give it more than once for several | Repositories, code, issues, pull requests |
66+| `org:acme` or `workspace:acme` | One workspace | Repositories, code, issues, pull requests |
67+| `language:rust` | Code in a language, or repositories mostly written in it. `ts`, `js`, `py` and `cpp` are understood | Repositories, code |
68+| `path:src/` | Code whose path contains `src/`. With `*`, a pattern: `path:*.rs`, `path:src/*/mod.rs` | Code |
69+| `is:issue`, `is:pr` | Only issues, or only pull requests | Issues, pull requests |
70+| `is:open`, `is:closed` | Issues and pull requests by state | Issues, pull requests |
71+| `is:merged`, `is:draft` | Pull requests that were merged, or are still drafts | Pull requests |
72+| `is:public`, `is:private` | Results from public or private repositories | Repositories, code, issues, pull requests |
73+| `author:ana` | Opened by someone | Issues, pull requests |
74+| `label:bug` | With a label. Quote a label with spaces: `label:"good first issue"` | Issues, pull requests |
75+| `type:code` | Which tab to open: `repositories`, `code`, `issues`, `pulls` or `people` | All |
76+
77+Without a tab or `type:`, the qualifiers choose one: `path:` opens Code,
78+`is:pr` opens Pull requests, `is:open`, `author:` or `label:` open Issues,
79+and anything else opens Repositories. A qualifier that only one kind of
80+result has rules the others out: a query with `path:` finds no people.
81+
82+Some examples:
83+
84+```text
85+fetchEvents language:typescript path:app/
86+"rate limit" org:acme is:issue is:open
87+crash author:ana -label:wontfix
88+router repo:acme/web repo:acme/api
89+```
90+
91+## What is indexed
92+
93+Search is kept current as things change:
94+
95+| What | When it is indexed |
96+| --- | --- |
97+| A repository's name, description, topics and README | When it is created or its settings change, and on every push to its default branch |
98+| Code | On every push to the default branch: only the files the push changed |
99+| Issues and pull requests | When they are opened, edited, assigned, closed, reopened, marked ready or merged |
100+| People and workspaces | When an account or a workspace is made, or its name, description or avatar changes |
101+
102+Code is indexed from default branches only. Some files are left out:
103+
104+- vendored and generated directories, such as `node_modules`, `vendor`,
105+ `third_party`, `dist`, `target` and `.next`;
106+- lockfiles, such as `package-lock.json`, `pnpm-lock.yaml`, `Cargo.lock`
107+ and `go.sum`;
108+- binary files (images, fonts, archives, compiled code) and minified files;
109+- files larger than 512 KB.
110+
111+A push that changes more than 300 files has the whole default branch
112+compared instead, and files are read a few dozen at a time in the
113+background, so very large pushes appear in search over a few minutes. A
114+repository's first 5,000 files are indexed.
115+
116+Set a repository's **topics** in **Settings → Repository**. Topics show on
117+the project's page and in search results, and each opens Explore for that
118+topic.
119+
120+## Who sees what
121+
122+Search shows you exactly what you could open yourself:
123+
124+- Public repositories, and their code, issues and pull requests, are
125+ shown to everyone, signed in or not.
126+- Private repositories, and their code, issues and pull requests, are shown
127+ only to members of the workspace that owns them, and to that workspace's
128+ agents.
129+- People and workspaces are public, as their pages are. Search never shows
130+ which workspaces someone belongs to.
131+
132+Visibility is checked when you search, against your memberships and each
133+repository's visibility as they are at that moment, not as they were when
134+something was indexed. A repository made private disappears from everyone
135+else's results at once; one made public appears in them within moments.
136+Someone removed from a workspace stops seeing its private results on their
137+next search.
138+
139+## Explore
140+
141+[Explore](https://g1t.sh/explore) lists public projects only:
142+
143+| View | Shows |
144+| --- | --- |
145+| **Recently active** | Projects by their last push to the default branch |
146+| **New** | Projects by when they were created |
147+| **Languages** | The languages public projects are mostly written in, with how many; choose one to see its projects |
148+| **Topics** | The topics public projects have, with how many; choose one to see its projects |
149+
150+A project's language is the one most of its indexed code is written in,
151+leaving out prose and data such as Markdown, JSON and YAML.
152+
153+## From the API and agents
154+
155+The same search is `GET /search` in the API and the `search` tool over
156+MCP. It takes `q` (the query), `type` and `page`, and needs no token for
157+public results:
158+
159+```sh
160+curl "https://api.g1t.sh/search?q=parse_query+language:rust&type=code"
161+```
162+
163+With a token, private results in your workspaces are included. g1t's own
164+agents can search too, with the token their run is given. See
165+[Search g1t](/reference/api/search/search/) in the API reference and
166+[MCP tools](/reference/mcp/#search).
167+
168+`search` looks across all of g1t. To ask about one workspace's catalog,
169+docs and memory, use the [context hub](/guides/context-hub/) and its
170+`search_context` tool.
+12−2
3838 | `list_repos` | | Repositories you can see, optionally filtered by `query`. | [`GET /repos?q=`](/reference/api/repositories/list-repos/) |
3939 | `get_repo` | `repo` | One repository's details. | [`GET /repos/{owner}/{name}`](/reference/api/repositories/get-repo/) |
4040 | `create_repo` | `name` | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `workspace` may be left out if you belong to exactly one. | [`POST /repos`](/reference/api/repositories/create-repo/) |
41−| `update_repo` | `repo` | Change its description, whether it is private, and whether its default branch is protected. Members only. | [`PATCH /repos/{owner}/{name}`](/reference/api/repositories/update-repo/) |
41+| `update_repo` | `repo` | Change its description, its `topics`, whether it is private, and whether its default branch is protected. Members only. | [`PATCH /repos/{owner}/{name}`](/reference/api/repositories/update-repo/) |
4242 | `get_repo_settings` | `repo` | How it handles pull requests: approvals, checks, being up to date, and how g1t's agents are reviewed, revised and merged. | [`GET /repos/{owner}/{name}/settings`](/reference/api/repositories/get-repo-settings/) |
4343 | `update_repo_settings` | `repo` | Change those settings. Only the fields given change. Members only. | [`PATCH /repos/{owner}/{name}/settings`](/reference/api/repositories/update-repo-settings/) |
4444 | `list_labels` | `repo` | The labels available on its issues. | [`GET /repos/{owner}/{name}/labels`](/reference/api/issues/list-labels/) |
4949 `max_revisions`, `auto_merge` and `merge_queue`. See
5050 [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for).
5151
52+## Search
53+
54+| Tool | Required | What it does | Route |
55+| --- | --- | --- | --- |
56+| `search` | `query` | Search all of g1t: repositories, code on default branches, issues, pull requests, people and workspaces. Public results for everyone; private ones in workspaces you belong to. `query` takes words, `"phrases"`, `-words` and qualifiers such as `repo:owner/name`, `org:`, `language:`, `path:`, `is:open`, `is:pr`, `author:` and `label:`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; `page` and `per_page` page through. Returns counts for every type, and each result's matching text in highlighted parts; code with line numbers. | [`GET /search`](/reference/api/search/search/) |
57+
58+See [search and Explore](/guides/search/) for the full syntax. `search`
59+looks across all of g1t; `search_context`, under [Memory](#memory), asks one
60+workspace's context hub.
61+
5262 ## Issues
5363
5464 | Tool | Required | What it does | Route |
185195
186196 | Run | Tools |
187197 | --- | --- |
188−| Implement, revise, answer | `get_repo`, `list_issues`, `get_issue`, `list_labels`, `list_pull_requests`, `get_pull_request`, `get_pull_request_changes`, `read_session`, `get_merge_queue`, `list_events`, `recall`, `search_context`, `get_entity`, `list_workflows`, `list_workflow_runs`, `get_workflow_run`, `get_job_logs`, and `create_issue`, `add_comment`, `take_messages`, `remember`, `message_agent`, `answer_message`, `get_context` |
198+| Implement, revise, answer | `get_repo`, `list_issues`, `get_issue`, `list_labels`, `list_pull_requests`, `get_pull_request`, `get_pull_request_changes`, `read_session`, `get_merge_queue`, `list_events`, `recall`, `search_context`, `get_entity`, `search`, `list_workflows`, `list_workflow_runs`, `get_workflow_run`, `get_job_logs`, and `create_issue`, `add_comment`, `take_messages`, `remember`, `message_agent`, `answer_message`, `get_context` |
189199 | Review | The same reading tools, and `add_comment`, `review_pull_request`, `get_context` |
190200 | Plan | The same reading tools, and `create_issue`, `get_context` |
191201 | Catch up | The reading tools only |
+195−1
107107 ]
108108 },
109109 {
110+ "name": "Search",
111+ "description": "One search across all of g1t: repositories, code, issues, pull requests, people and workspaces. Public content for everyone, and private content in workspaces you belong to.",
112+ "x-tools": [
113+ "search"
114+ ]
115+ },
116+ {
110117 "name": "Context",
111118 "description": "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.",
112119 "x-tools": [
703710 }
704711 }
705712 },
713+ "/search": {
714+ "get": {
715+ "operationId": "search",
716+ "tags": [
717+ "Search"
718+ ],
719+ "summary": "Search g1t",
720+ "description": "Search all of g1t: repositories (name, description, topics, README), code on default branches (file names and contents), issues, pull requests, people and workspaces. Covers everything public, and private content in workspaces you belong to; signed out, public only. Write words, \"exact phrases\", -words to leave out, and qualifiers: repo:owner/name, org:workspace, language:rust, path:src/ (a glob with *), is:issue, is:pr, is:open, is:closed, is:merged, author:username, label:bug. type picks the kind of results (repositories, code, issues, pulls or people); without it, the qualifiers decide. Returns one page of results with the matches highlighted, code with line numbers, and how many there are of each kind.\n\n`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to members of its workspace, checked when the search runs, so a repository made private stops appearing at once. A g1t agent's token can search too.",
721+ "x-mcp-tool": "search",
722+ "security": [
723+ {
724+ "token": []
725+ },
726+ {}
727+ ],
728+ "parameters": [
729+ {
730+ "name": "q",
731+ "in": "query",
732+ "required": true,
733+ "schema": {
734+ "type": "string"
735+ },
736+ "description": "What to look for: words, \"phrases\" and qualifiers, such as parse_query language:rust repo:acme/web."
737+ },
738+ {
739+ "name": "type",
740+ "in": "query",
741+ "required": false,
742+ "schema": {
743+ "type": "string",
744+ "enum": [
745+ "repositories",
746+ "code",
747+ "issues",
748+ "pulls",
749+ "people"
750+ ]
751+ },
752+ "description": "Which kind of results. Worked out from the qualifiers if not given: path: means code, is:pr pull requests, is:open or label: issues, otherwise repositories."
753+ },
754+ {
755+ "name": "page",
756+ "in": "query",
757+ "required": false,
758+ "schema": {
759+ "type": "integer"
760+ },
761+ "description": "From 1; at most 50."
762+ },
763+ {
764+ "name": "per_page",
765+ "in": "query",
766+ "required": false,
767+ "schema": {
768+ "type": "integer"
769+ },
770+ "description": "At most 50; 20 if not given."
771+ }
772+ ],
773+ "responses": {
774+ "200": {
775+ "description": "Success.",
776+ "content": {
777+ "application/json": {
778+ "schema": {},
779+ "example": {
780+ "query": "parse_query repo:acme/web language:rust",
781+ "type": "code",
782+ "counts": {
783+ "repositories": 0,
784+ "code": 2,
785+ "issues": 0,
786+ "pulls": 0,
787+ "people": 0
788+ },
789+ "page": 1,
790+ "per_page": 20,
791+ "more": false,
792+ "hits": [
793+ {
794+ "kind": "code",
795+ "title": "src/search/query.rs",
796+ "url": "/acme/web/blob/main/src/search/query.rs#L42",
797+ "repo": "acme/web",
798+ "private": false,
799+ "description": null,
800+ "snippet": [],
801+ "lines": [
802+ {
803+ "number": 41,
804+ "parts": [
805+ {
806+ "text": "/// Reads a query typed into the search box.",
807+ "highlight": false
808+ }
809+ ]
810+ },
811+ {
812+ "number": 42,
813+ "parts": [
814+ {
815+ "text": "pub fn ",
816+ "highlight": false
817+ },
818+ {
819+ "text": "parse_query",
820+ "highlight": true
821+ },
822+ {
823+ "text": "(text: &str) -> Query {",
824+ "highlight": false
825+ }
826+ ]
827+ },
828+ {
829+ "number": 43,
830+ "parts": [
831+ {
832+ "text": " let mut query = Query::default();",
833+ "highlight": false
834+ }
835+ ]
836+ }
837+ ],
838+ "path": "src/search/query.rs",
839+ "language": "rust",
840+ "ref": "main",
841+ "number": null,
842+ "state": null,
843+ "author": null,
844+ "labels": [],
845+ "topics": [],
846+ "slug": null,
847+ "avatar": null,
848+ "updated_at": null
849+ }
850+ ],
851+ "notes": []
852+ }
853+ }
854+ }
855+ },
856+ "401": {
857+ "description": "A token is required, or the one sent is not valid.",
858+ "content": {
859+ "application/json": {
860+ "schema": {
861+ "$ref": "#/components/schemas/Error"
862+ }
863+ }
864+ }
865+ },
866+ "403": {
867+ "description": "Signed in, but not allowed to do this.",
868+ "content": {
869+ "application/json": {
870+ "schema": {
871+ "$ref": "#/components/schemas/Error"
872+ }
873+ }
874+ }
875+ },
876+ "422": {
877+ "description": "The input is not valid.",
878+ "content": {
879+ "application/json": {
880+ "schema": {
881+ "$ref": "#/components/schemas/Error"
882+ }
883+ }
884+ }
885+ }
886+ },
887+ "x-example-query": {
888+ "q": "parse_query language:rust repo:acme/web",
889+ "type": "code"
890+ }
891+ }
892+ },
706893 "/repos/{owner}/{name}": {
707894 "get": {
708895 "operationId": "get_repo",
807994 "Repositories"
808995 ],
809996 "summary": "Update a repository",
810− "description": "Change a repository's description, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only.",
997+ "description": "Change a repository's description, its topics, whether it is private, and whether its default branch is protected. A protected branch refuses pushes and changes only by merging a pull request. Only the fields given are changed. Members of its workspace only.",
811998 "x-mcp-tool": "update_repo",
812999 "security": [
8131000 {
9231110 "protected": {
9241111 "type": "boolean",
9251112 "description": "Refuse pushes to the default branch, so that it changes only by merging a pull request."
1113+ },
1114+ "topics": {
1115+ "type": "array",
1116+ "items": {
1117+ "type": "string"
1118+ },
1119+ "description": "Replaces its topics, which search and Explore show: lowercase letters, digits and hyphens, at most 20. An empty list clears them."
9261120 }
9271121 }
9281122 },
+200−0
1+import { Command as Cmdk } from "cmdk";
2+import { Code2, CornerDownLeft, Search } from "lucide-react";
3+import { Dialog as Primitive } from "radix-ui";
4+import { type ReactNode, useEffect, useRef, useState } from "react";
5+import { useFetcher, useNavigate } from "react-router";
6+
7+import type { SiteHit } from "@g1t/contracts";
8+
9+import { searchHref } from "../lib/search";
10+import { hitIcon } from "./search";
11+import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "./ui/combobox";
12+
13+/** A page or action the palette can jump to. */
14+export type PaletteCommand = { label: string; hint?: string; to: string; icon: ReactNode };
15+
16+/** How long typing has to pause before results are asked for. */
17+const PAUSE_MS = 140;
18+
19+/** Opens and closes the palette on ⌘K or Ctrl-K, from anywhere on the page. */
20+export function usePaletteShortcut(toggle: () => void) {
21+ const latest = useRef(toggle);
22+ latest.current = toggle;
23+ useEffect(() => {
24+ const onKey = (event: KeyboardEvent) => {
25+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
26+ event.preventDefault();
27+ latest.current();
28+ }
29+ };
30+ window.addEventListener("keydown", onKey);
31+ return () => window.removeEventListener("keydown", onKey);
32+ }, []);
33+}
34+
35+function hitHint(hit: SiteHit): string {
36+ switch (hit.kind) {
37+ case "repository":
38+ return "Repository";
39+ case "issue":
40+ return `${hit.repo} #${hit.number}`;
41+ case "pull":
42+ return `${hit.repo} #${hit.number}`;
43+ case "user":
44+ return `Person · ${hit.slug}`;
45+ case "workspace":
46+ return `Workspace · ${hit.slug}`;
47+ default:
48+ return hit.repo ?? "";
49+ }
50+}
51+
52+/**
53+ * ⌘K: pages and actions from what the page already knows, and, as someone
54+ * types, repositories, issues, pull requests and people from search,
55+ * public ones for anyone and private ones for their workspaces' members.
56+ * The first row always searches all of g1t for what was typed.
57+ */
58+export function CommandPalette({
59+ open,
60+ onOpenChange,
61+ commands,
62+ repo,
63+}: {
64+ open: boolean;
65+ onOpenChange: (open: boolean) => void;
66+ commands: PaletteCommand[];
67+ /** The repository being looked at, `owner/name`, to offer searching its code. */
68+ repo?: string | null;
69+}) {
70+ const navigate = useNavigate();
71+ const fetcher = useFetcher<{ q: string; hits: SiteHit[] }>();
72+ const [query, setQuery] = useState("");
73+ const trimmed = query.trim();
74+
75+ useEffect(() => {
76+ if (open) setQuery("");
77+ }, [open]);
78+ useEffect(() => {
79+ if (trimmed.length < 2) return;
80+ const timer = setTimeout(() => fetcher.load(`/search.json?q=${encodeURIComponent(trimmed)}`), PAUSE_MS);
81+ return () => clearTimeout(timer);
82+ // `fetcher` changes identity as it loads; only the text should restart it.
83+ // eslint-disable-next-line react-hooks/exhaustive-deps
84+ }, [trimmed]);
85+
86+ const hits = trimmed.length >= 2 && fetcher.data?.q === trimmed ? fetcher.data.hits : [];
87+ const go = (to: string) => {
88+ onOpenChange(false);
89+ // The docs and other sites are full page loads.
90+ if (/^https?:\/\//.test(to)) window.location.assign(to);
91+ else navigate(to);
92+ };
93+ const words = trimmed.toLowerCase().split(/\s+/).filter(Boolean);
94+ const pages = commands
95+ .filter((command) => {
96+ const text = `${command.label} ${command.hint ?? ""}`.toLowerCase();
97+ return words.every((word) => text.includes(word));
98+ })
99+ .slice(0, trimmed ? 8 : 12);
100+ const itemClass = "gap-3 rounded-lg px-3 py-2";
101+
102+ return (
103+ <Primitive.Root open={open} onOpenChange={onOpenChange}>
104+ <Primitive.Portal>
105+ <Primitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out motion-reduce:animate-none" />
106+ <Primitive.Content
107+ aria-describedby={undefined}
108+ className="fixed top-[12vh] left-1/2 z-50 w-[calc(100vw-2rem)] max-w-xl -translate-x-1/2 overflow-hidden rounded-xl border border-line-strong bg-raised shadow-2xl shadow-black/60 outline-none"
109+ >
110+ <Primitive.Title className="sr-only">Go to or search</Primitive.Title>
111+ {/* Filtering is done here: search already filtered its results. */}
112+ <Command shouldFilter={false} loop>
113+ <div className="flex items-center gap-3 border-b border-line px-4">
114+ <Search size={16} className="shrink-0 text-faint" />
115+ <Cmdk.Input
116+ value={query}
117+ onValueChange={setQuery}
118+ placeholder="Search g1t, or go to a page…"
119+ autoComplete="off"
120+ data-1p-ignore
121+ className="h-12 grow border-0 bg-transparent text-sm shadow-none outline-none placeholder:text-faint focus-visible:outline-none"
122+ />
123+ <kbd className="rounded border border-line px-1.5 font-mono text-[0.6875rem] text-faint">esc</kbd>
124+ </div>
125+ <CommandList className="max-h-[55vh] p-1.5">
126+ <CommandEmpty>Nothing matches.</CommandEmpty>
127+ {trimmed && (
128+ <CommandGroup heading="Search">
129+ <CommandItem value="search:all" onSelect={() => go(searchHref(trimmed))} className={itemClass}>
130+ <Search />
131+ <span className="min-w-0 grow truncate">
132+ Search g1t for <span className="font-medium text-fg">“{trimmed}”</span>
133+ </span>
134+ <CornerDownLeft className="size-3.5! text-faint!" />
135+ </CommandItem>
136+ <CommandItem value="search:code" onSelect={() => go(searchHref(trimmed, "code"))} className={itemClass}>
137+ <Code2 />
138+ <span className="min-w-0 grow truncate">
139+ Search code for <span className="font-medium text-fg">“{trimmed}”</span>
140+ </span>
141+ </CommandItem>
142+ {repo && (
143+ <CommandItem
144+ value="search:repo"
145+ onSelect={() => go(searchHref(`${trimmed} repo:${repo}`, "code"))}
146+ className={itemClass}
147+ >
148+ <Code2 />
149+ <span className="min-w-0 grow truncate">
150+ Search <span className="font-mono">{repo}</span> for{" "}
151+ <span className="font-medium text-fg">“{trimmed}”</span>
152+ </span>
153+ </CommandItem>
154+ )}
155+ </CommandGroup>
156+ )}
157+ {hits.length > 0 && (
158+ <CommandGroup heading="Results">
159+ {hits.map((hit) => (
160+ <CommandItem
161+ key={`${hit.kind}:${hit.url}`}
162+ value={`hit:${hit.kind}:${hit.url}`}
163+ onSelect={() => go(hit.url)}
164+ className={itemClass}
165+ >
166+ {hitIcon(hit)}
167+ <span className="min-w-0 grow truncate">
168+ {hit.kind === "repository" ? <span className="font-mono">{hit.title}</span> : hit.title}
169+ </span>
170+ <span className="shrink-0 truncate text-xs text-faint">{hitHint(hit)}</span>
171+ </CommandItem>
172+ ))}
173+ </CommandGroup>
174+ )}
175+ {pages.length > 0 && (
176+ <CommandGroup heading="Go to">
177+ {pages.map((command) => (
178+ <CommandItem
179+ key={`${command.to}-${command.label}`}
180+ value={`page:${command.to}:${command.label}`}
181+ onSelect={() => go(command.to)}
182+ className={itemClass}
183+ >
184+ <span className="shrink-0 text-faint">{command.icon}</span>
185+ <span className="min-w-0 grow truncate">{command.label}</span>
186+ {command.hint && <span className="shrink-0 truncate text-xs text-faint">{command.hint}</span>}
187+ </CommandItem>
188+ ))}
189+ </CommandGroup>
190+ )}
191+ </CommandList>
192+ </Command>
193+ {hits.length === 0 && fetcher.state === "loading" && trimmed.length >= 2 && (
194+ <p className="border-t border-line px-4 py-2 text-xs text-faint">Searching…</p>
195+ )}
196+ </Primitive.Content>
197+ </Primitive.Portal>
198+ </Primitive.Root>
199+ );
200+}
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.