From 8e2feac18e2e6ed063310051720a0eb11cebce97 Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Sat, 15 Aug 2026 11:41:04 +0200 Subject: [PATCH] Fix repository paths containing spaces (#62) --- TESTING.md | 7 ++-- crates/gitea/src/repositories.rs | 31 ++++++++++-------- crates/gitea/src/repositories/tests.rs | 45 +++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/TESTING.md b/TESTING.md index dbf523d..6708344 100644 --- a/TESTING.md +++ b/TESTING.md @@ -374,9 +374,10 @@ answer before uploading it to App Store Connect. - [ ] Switch a repository between **History** and **Files**. Each mode displays the expected content and switching back preserves normal navigation. -- [ ] In Files, traverse several nested folders using rows, the navigation-bar - Back button, and the left-edge swipe. Folder contents and titles match the - repository hierarchy. +- [ ] In Files, traverse several nested folders using rows, including a folder + and file whose names contain spaces (for example below Chezmoi's + `private_Library`), the navigation-bar Back button, and the left-edge + swipe. Folder contents, files, and titles match the repository hierarchy. - [ ] In the repository root and in several nested folders, switch between **Files** and **History**. Each folder history contains only commits that affect that folder or its descendants. Switch back to Files and verify the diff --git a/crates/gitea/src/repositories.rs b/crates/gitea/src/repositories.rs index 3969ce1..adf0642 100644 --- a/crates/gitea/src/repositories.rs +++ b/crates/gitea/src/repositories.rs @@ -12,6 +12,10 @@ use crate::{ }; use gitea_openapi::apis; +fn encode_path(path: &str) -> String { + apis::urlencode(path).replace('+', "%20") +} + impl Client { pub async fn current_user_repositories_page( &self, @@ -99,7 +103,7 @@ impl Client { "repos/{}/{}/contents/{}", apis::urlencode(&repository.owner), apis::urlencode(&repository.repository), - apis::urlencode(path), + encode_path(path), ); self.execute(self.request(Method::GET, &endpoint)?) .await? @@ -110,19 +114,18 @@ impl Client { } pub async fn repository_file(&self, repository: &RepositoryId, path: &str) -> Result> { - apis::repository_api::repo_get_raw_file( - &self.configuration(), - &repository.owner, - &repository.repository, - path, - None, - ) - .await - .map_err(Error::generated)? - .bytes() - .await - .map(|bytes| bytes.to_vec()) - .map_err(Into::into) + let endpoint = format!( + "repos/{}/{}/raw/{}", + apis::urlencode(&repository.owner), + apis::urlencode(&repository.repository), + encode_path(path), + ); + self.execute(self.request(Method::GET, &endpoint)?) + .await? + .bytes() + .await + .map(|bytes| bytes.to_vec()) + .map_err(Into::into) } pub async fn branch_history( diff --git a/crates/gitea/src/repositories/tests.rs b/crates/gitea/src/repositories/tests.rs index 39f6f58..1129367 100644 --- a/crates/gitea/src/repositories/tests.rs +++ b/crates/gitea/src/repositories/tests.rs @@ -110,7 +110,7 @@ async fn reads_nested_directory_from_compatible_contents_endpoint() { let mut buffer = [0; 4096]; let length = stream.read(&mut buffer).unwrap(); let request = String::from_utf8_lossy(&buffer[..length]).into_owned(); - let body = r#"[{"name":"guide.md","path":"docs/guide.md","type":"file"}]"#; + let body = r#"[{"name":"guide.md","path":"docs/private guide/guide.md","type":"file"}]"#; write!( stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", @@ -125,16 +125,53 @@ async fn reads_nested_directory_from_compatible_contents_endpoint() { .unwrap(); let repository = RepositoryId::new("forgejo", "forgejo").unwrap(); let contents = client - .repository_contents(&repository, "docs/guide") + .repository_contents(&repository, "docs/private guide") .await .unwrap(); assert_eq!(contents.len(), 1); - assert_eq!(contents[0].path.as_deref(), Some("docs/guide.md")); + assert_eq!( + contents[0].path.as_deref(), + Some("docs/private guide/guide.md") + ); assert!( server .join() .unwrap() - .starts_with("GET /api/v1/repos/forgejo/forgejo/contents/docs%2Fguide ") + .starts_with("GET /api/v1/repos/forgejo/forgejo/contents/docs%2Fprivate%20guide ") + ); +} + +#[tokio::test] +async fn reads_file_with_spaces_from_raw_endpoint() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buffer = [0; 4096]; + let length = stream.read(&mut buffer).unwrap(); + let request = String::from_utf8_lossy(&buffer[..length]).into_owned(); + let body = "contents"; + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .unwrap(); + request + }); + + let client = Client::new(&format!("http://{address}"), None).unwrap(); + let repository = RepositoryId::new("gitea", "gitea").unwrap(); + let contents = client + .repository_file(&repository, "docs/private guide/read me.md") + .await + .unwrap(); + + assert_eq!(contents, b"contents"); + assert!( + server.join().unwrap().starts_with( + "GET /api/v1/repos/gitea/gitea/raw/docs%2Fprivate%20guide%2Fread%20me.md " + ) ); }