Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/api/src/domain/repositories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3053,7 +3053,7 @@ pub async fn repository_file_finder_for_actor_by_owner_name(
let resolved_ref = resolve_repository_ref(pool, &repository, query.ref_name).await?;
let normalized_query = query.query.unwrap_or("").trim().to_lowercase();
let page = query.page.max(1);
let page_size = query.page_size.clamp(1, 100);
let page_size = query.page_size.clamp(1, 5_000);
let files = list_repository_files_for_resolved_ref(pool, repository.id, &resolved_ref).await?;
refresh_repository_ref_files_cache(pool, repository.id, &resolved_ref, &files).await?;
let mut items = files
Expand Down
11 changes: 8 additions & 3 deletions crates/api/src/routes/repositories.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use axum::{
body::Bytes,
extract::{Path, Query, State},
extract::{OriginalUri, Path, Query, State},
http::{header, HeaderMap, HeaderValue, StatusCode},
response::{IntoResponse, Response},
routing::{delete, get, patch, post, put},
Expand Down Expand Up @@ -4433,21 +4433,26 @@ async fn release_reaction(
async fn file_finder(
State(state): State<AppState>,
headers: HeaderMap,
OriginalUri(original_uri): OriginalUri,
Path((owner, repo)): Path<(String, String)>,
Query(query): Query<FileFinderQuery>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorEnvelope>)> {
let actor = AuthenticatedUser::from_headers(&state, &headers).await?;
let pool = state.db.as_ref().ok_or_else(database_unavailable)?;
let is_path_cache_contract = original_uri.path().ends_with("/find");
let envelope = repository_file_finder_for_actor_by_owner_name(
pool,
actor.0.id,
&owner,
&repo,
RepositoryFileFinderQuery {
ref_name: query.ref_name.as_deref(),
query: query.q.as_deref(),
query: if is_path_cache_contract { None } else { query.q.as_deref() },
page: query.page.unwrap_or(1).max(1),
page_size: query.page_size.unwrap_or(20).clamp(1, 100),
page_size: query
.page_size
.unwrap_or(if is_path_cache_contract { 5_000 } else { 20 })
.clamp(1, 5_000),
},
)
.await
Expand Down
40 changes: 40 additions & 0 deletions crates/api/tests/repository_tree_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,46 @@ async fn repository_tree_contract_resolves_branches_tags_and_recovery_links() {
assert_eq!(finder_page_body["total"], 105);
assert_eq!(finder_page_body["items"][0]["path"], "docs/example-040.md");

let (path_cache_status, path_cache_body) = send_json(
app.clone(),
&format!("{base}/find?ref={encoded_feature}&q=guide"),
Some(&owner_cookie),
)
.await;
assert_eq!(path_cache_status, StatusCode::OK);
assert_eq!(path_cache_body["resolvedRef"]["shortName"], "feature/tree-nav");
assert_eq!(
path_cache_body["total"], 107,
"the /find path-cache contract returns the full ref path list and ignores q"
);
assert_eq!(path_cache_body["pageSize"], 5000);
assert!(path_cache_body["items"]
.as_array()
.expect("path cache items should be an array")
.iter()
.any(|entry| entry["path"] == "README.md"));
assert!(path_cache_body["items"]
.as_array()
.expect("path cache items should be an array")
.iter()
.any(|entry| entry["path"] == "docs/example-104.md"));
let cached_paths: serde_json::Value = sqlx::query_scalar(
r#"
SELECT paths
FROM repository_ref_files
WHERE repository_id = $1 AND ref = 'feature/tree-nav'
"#,
)
.bind(repository.id)
.fetch_one(&pool)
.await
.expect("finder request should refresh repository_ref_files");
assert!(cached_paths
.as_array()
.expect("cached paths should be a JSON array")
.iter()
.any(|path| path == "docs/guide.md"));

let (bad_path_status, bad_path_body) = send_json(
app.clone(),
&format!("{base}/contents/%2E%2E/secrets?ref={encoded_feature}"),
Expand Down
3 changes: 2 additions & 1 deletion prd.json
Original file line number Diff line number Diff line change
Expand Up @@ -2171,7 +2171,8 @@
"repocode-002",
"search-005"
],
"build_pass": true
"build_pass": true,
"qa_pass": true
},
{
"id": "security-002",
Expand Down
37 changes: 33 additions & 4 deletions qa-report-summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -3404,12 +3404,41 @@
},
{
"feature_id": "search-007",
"description": "File finder widget \u2014 keyboard-driven `t` shortcut on a repo to fuzzy-find a file",
"description": "File finder widget \u2014 keyboard-driven `t` shortcut on a repo to fuzzy-find a file by path within the current ref.",
"category": "feature",
"qa_pass": false,
"attempts": 0,
"qa_pass": true,
"attempts": 1,
"exhausted": false,
"sub_phases": {}
"sub_phases": {
"functional": {
"status": "pass",
"notes": "Dedicated finder page now loads the current ref path cache, focuses the combobox, shows the full cached list for empty input, performs local fuzzy scoring/highlighting, supports ArrowUp/ArrowDown and Enter open, and preserves concrete blob links."
},
"api_contract": {
"status": "pass",
"endpoints_tested": [
"GET /api/repos/:owner/:repo/find?ref=feature%2Ftree-nav&q=guide",
"GET /api/repos/:owner/:repo/file-finder?ref=feature%2Ftree-nav&q=guide"
],
"notes": "DB-backed Rust contract verified /find ignores q and returns all 107 custom ref paths with pageSize 5000, while legacy /file-finder filtering/pagination remains covered; repository_ref_files JSON cache row is refreshed for the resolved ref."
},
"security": {
"status": "pass",
"checks": [
"authenticated repository access boundary via existing route extractor",
"no server-side filtering on /find",
"no dead links",
"no stack/secret leakage in focused gates"
],
"notes": "The finder API continues to require AuthenticatedUser and repository permission checks through repository_file_finder_for_actor_by_owner_name; browser/API gates used real signed sessions and did not expose internal data."
},
"accessibility": {
"status": "pass",
"violations": [],
"notes": "Combobox/listbox/option roles, aria-selected, aria-activedescendant, focused input, keyboard navigation, no dead controls, and no horizontal overflow were verified in unit and Playwright coverage."
}
},
"overall_status": "pass"
},
{
"feature_id": "security-002",
Expand Down
94 changes: 94 additions & 0 deletions qa-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -6898,5 +6898,99 @@
"ralph/screenshots/build/settings-005-final-secrets-mobile.jpg",
"ralph/screenshots/build/settings-005-final-secrets-forbidden.jpg"
]
},
{
"feature_id": "search-007",
"attempt": 1,
"status": "pass",
"description": "File finder widget \u2014 keyboard-driven `t` shortcut on a repo to fuzzy-find a file by path within the current ref.",
"category": "feature",
"qa_pass": true,
"attempts": 1,
"exhausted": false,
"timestamp": "2026-05-14T18:58:00Z",
"tested_steps": [
"make doctor passed with postgres-test up, .env.test present, and CARGO_TARGET_DIR on .scratch/cargo-target.",
"DB-backed Rust/API contract repository_tree_navigation::repository_tree_contract_resolves_branches_tags_and_recovery_links passed with TEST_DATABASE_URL loaded; verified /api/repos/:owner/:repo/find ignores q, returns the full ref path list, and refreshes repository_ref_files.",
"make test passed: Cargo tests passed and Web tests passed (671 passed).",
"Focused Vitest repository-file-finder-page.test.tsx passed; covers cached path rendering, client-side fuzzy scoring, matched-character highlighting, ArrowUp/ArrowDown selection, Enter open, Escape clear, empty state, and concrete links.",
"System-Chrome Playwright repository-file-finder.spec.ts passed against real Next/Rust servers and .env.test; verified repo t shortcut, /find/<ref> page, focused input, empty query full list, no-match state, ArrowDown navigation, Enter open, concrete blob href, no dead controls, and no horizontal overflow.",
"make check passed: Cargo check, Web typecheck, Clippy, and Biome."
],
"bugs_found": [
{
"severity": "major",
"description": "The /api/repos/:owner/:repo/find alias reused the legacy /file-finder server-side q filtering/default page-size behavior instead of returning the unfiltered cached ref path list required for local fuzzy scoring.",
"file": "crates/api/src/routes/repositories.rs"
},
{
"severity": "major",
"description": "The dedicated finder page fetched the legacy file-finder endpoint with pageSize 100 instead of the /find path-cache contract, so larger refs were truncated before client-side fuzzy matching.",
"file": "web/src/app/[owner]/[repo]/find/[ref]/page.tsx"
},
{
"severity": "minor",
"description": "The finder result list was capped to the first 80 client-side matches, violating the empty-input full-list requirement for moderately sized refs.",
"file": "web/src/components/RepositoryFileFinderPage.tsx"
},
{
"severity": "minor",
"description": "ArrowDown on an empty result set could move the active index negative and aria-activedescendant could point at a mismatched option after bounds clamping.",
"file": "web/src/components/RepositoryFileFinderPage.tsx"
}
],
"fix_description": "Split the /find path-cache contract from legacy /file-finder filtering, raised the path-cache page-size path through domain code, taught the Next API helper/page to call /find with pathCache, removed the client 80-result cap, hardened active-option bounds, and added DB-backed API, Vitest, and system-Chrome Playwright coverage.",
"sub_phases": {
"functional": {
"status": "pass",
"notes": "Dedicated finder page now loads the current ref path cache, focuses the combobox, shows the full cached list for empty input, performs local fuzzy scoring/highlighting, supports ArrowUp/ArrowDown and Enter open, and preserves concrete blob links."
},
"api_contract": {
"status": "pass",
"endpoints_tested": [
"GET /api/repos/:owner/:repo/find?ref=feature%2Ftree-nav&q=guide",
"GET /api/repos/:owner/:repo/file-finder?ref=feature%2Ftree-nav&q=guide"
],
"notes": "DB-backed Rust contract verified /find ignores q and returns all 107 custom ref paths with pageSize 5000, while legacy /file-finder filtering/pagination remains covered; repository_ref_files JSON cache row is refreshed for the resolved ref."
},
"security": {
"status": "pass",
"checks": [
"authenticated repository access boundary via existing route extractor",
"no server-side filtering on /find",
"no dead links",
"no stack/secret leakage in focused gates"
],
"notes": "The finder API continues to require AuthenticatedUser and repository permission checks through repository_file_finder_for_actor_by_owner_name; browser/API gates used real signed sessions and did not expose internal data."
},
"accessibility": {
"status": "pass",
"violations": [],
"notes": "Combobox/listbox/option roles, aria-selected, aria-activedescendant, focused input, keyboard navigation, no dead controls, and no horizontal overflow were verified in unit and Playwright coverage."
}
},
"verification": {
"commands": [
"make doctor",
"TEST_DATABASE_URL=postgresql://opengithub:opengithub@localhost:55433/opengithub_test DATABASE_URL=postgresql://opengithub:opengithub@localhost:55433/opengithub_test DB_SSL=false ./hack/cargo_locked.sh test -p opengithub-api --test repository_tree_navigation repository_tree_contract_resolves_branches_tags_and_recovery_links -- --nocapture",
"cd web && npx vitest run tests/repository-file-finder-page.test.tsx",
"make test",
"cd web && TEST_DATABASE_URL=postgresql://opengithub:opengithub@localhost:55433/opengithub_test DATABASE_URL=postgresql://opengithub:opengithub@localhost:55433/opengithub_test DB_SSL=false SESSION_SECRET=playwright-session-secret-with-enough-entropy SESSION_COOKIE_NAME=og_session ./node_modules/.bin/playwright test -c ../.scratch/playwright-search-007.config.cjs repository-file-finder.spec.ts --project=system-chrome",
"make check"
],
"artifacts": [
"ralph/screenshots/build/search-007-file-finder-final.jpg"
]
},
"overall_status": "pass",
"blockers": [],
"gates": [
"make doctor: pass",
"focused DB-backed Rust/API contract: pass 1/1, no self-skip",
"focused Vitest file finder component/unit: pass 2/2",
"make test: pass (Cargo tests; Web tests 671 passed)",
"focused env-loaded system-Chrome Playwright: pass 1/1",
"make check: pass"
]
}
]
3 changes: 2 additions & 1 deletion web/src/app/[owner]/[repo]/find/[ref]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export default async function RepositoryFindPage({
getRepository(ownerLogin, repositoryName),
getRepositoryFileFinder(ownerLogin, repositoryName, refName, "", {
page: 1,
pageSize: 100,
pageSize: 5000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fetch the entire path cache for file finder

On refs with more than 5,000 files, this page fetches only page 1 with pageSize=5000 and then performs all fuzzy filtering against that truncated finder.items array. That means paths after the first 5,000 can never appear, be matched, or be opened, even though the /find contract for search-007 is to fuzzy-match locally against the full ref path list; the page needs to paginate through the cache or use an unpaginated path-cache response.

Useful? React with 👍 / 👎.

pathCache: true,
}),
])
: [null, null];
Expand Down
15 changes: 9 additions & 6 deletions web/src/components/RepositoryFileFinderPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,12 @@ export function RepositoryFileFinderPage({
return scored;
}, [finder.items, query]);

const visibleFiles = scoredFiles.slice(0, 80);
const activeFile =
visibleFiles[Math.min(activeIndex, visibleFiles.length - 1)];
const visibleFiles = scoredFiles;
const activeOptionIndex = Math.min(
Math.max(activeIndex, 0),
Math.max(visibleFiles.length - 1, 0),
);
const activeFile = visibleFiles[activeOptionIndex];

useEffect(() => {
inputRef.current?.focus();
Expand Down Expand Up @@ -163,7 +166,7 @@ export function RepositoryFileFinderPage({
<input
aria-activedescendant={
activeFile
? `repo-file-finder-result-${activeIndex}`
? `repo-file-finder-result-${activeOptionIndex}`
: undefined
}
aria-controls="repo-file-finder-page-results"
Expand All @@ -179,7 +182,7 @@ export function RepositoryFileFinderPage({
if (event.key === "ArrowDown") {
event.preventDefault();
setActiveIndex((index) =>
Math.min(visibleFiles.length - 1, index + 1),
Math.min(Math.max(visibleFiles.length - 1, 0), index + 1),
);
}
if (event.key === "ArrowUp") {
Expand Down Expand Up @@ -233,7 +236,7 @@ export function RepositoryFileFinderPage({
role="listbox"
>
{visibleFiles.map((file, index) => {
const active = index === activeIndex;
const active = index === activeOptionIndex;
return (
<Link
aria-selected={active}
Expand Down
6 changes: 3 additions & 3 deletions web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19059,10 +19059,10 @@ export async function getRepositoryFileFinderFromCookie(
repo: string,
refName: string,
query: string,
options: { page?: number; pageSize?: number } = {},
options: { page?: number; pageSize?: number; pathCache?: boolean } = {},
): Promise<RepositoryFileFinderResult | null> {
const params = new URLSearchParams({ ref: refName });
if (query.trim()) {
if (!options.pathCache && query.trim()) {
params.set("q", query.trim());
}
if (options.page) {
Expand All @@ -19074,7 +19074,7 @@ export async function getRepositoryFileFinderFromCookie(
let response: Response;
try {
response = await fetch(
`${apiBaseUrl()}/api/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/file-finder?${params.toString()}`,
`${apiBaseUrl()}/api/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${options.pathCache ? "find" : "file-finder"}?${params.toString()}`,
{
headers: cookie ? { cookie } : undefined,
cache: "no-store",
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/server-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ export async function getRepositoryFileFinder(
repo: string,
refName: string,
query = "",
options: { page?: number; pageSize?: number } = {},
options: { page?: number; pageSize?: number; pathCache?: boolean } = {},
) {
const requestHeaders = await headers();
return getRepositoryFileFinderFromCookie(
Expand Down
Loading