Start cloud threads with the local GitHub project (#60007)

## What changed

Pass the local project's GitHub origin to cloud prewarming so new cloud threads can provision a repository checkout. Preserve the attached local project when switching backends and resolve its origin for each thread start.

Normalize supported GitHub origins to credential-free HTTPS URLs and select a repository-specific cloud working directory. Unsupported origins retain the blank environment behavior. Keep project metadata outside the app-server wire protocol, and fix HTTP Git URL parsing so query strings and fragments cannot be mistaken for authority text.

## Testing

Add coverage for project routing, origin normalization and rejection, managed environment encoding, local project retention, and Git URL credential stripping.

GitOrigin-RevId: 5859769965faf060461ccc1cbb3fd48f8e2f2155
This commit is contained in:
Benjamin Carlsson
2026-11-06 23:00:38 +00:00
committed by copyberry
parent df769889c0
commit 7de8a0afc0
21 changed files with 491 additions and 15 deletions
+7 -3
View File
@@ -77,9 +77,13 @@ impl TryFrom<&str> for SanitizedGitUrl {
// `file://` authority as its host and does not expose its userinfo.
// Rebuilding only this part also preserves escaped repository paths.
if let Some((scheme, authority_and_path)) = value.split_once("://") {
let authority_end = authority_and_path
.find('/')
.unwrap_or(authority_and_path.len());
// Git treats '?' and '#' as authority text on non-HTTP transports.
let authority_end = if matches!(scheme, "http" | "https") {
authority_and_path.find(['/', '?', '#'])
} else {
authority_and_path.find('/')
}
.unwrap_or(authority_and_path.len());
let (authority, path) = authority_and_path.split_at(authority_end);
let Some((_, host)) = authority.rsplit_once('@') else {
return Ok(Self(original.to_owned()));
@@ -15,6 +15,30 @@ fn removes_https_username_and_password() {
assert_eq!(url.as_str(), "https://github.com/org/repo.git");
}
#[test]
fn http_query_and_fragment_are_outside_the_authority() {
for (scheme, suffix) in [("http", "?@github.com/a/b"), ("https", "#@github.com/a/b")] {
let expected = format!("{scheme}://example.com{suffix}");
for authority in ["example.com", "user:password@example.com"] {
let remote = format!("{scheme}://{authority}{suffix}");
let sanitized = SanitizedGitUrl::try_from(remote.as_str()).expect("parse remote URL");
assert_eq!(sanitized.as_str(), expected);
}
}
}
#[test]
fn non_http_credentials_can_contain_query_and_fragment_delimiters() {
for scheme in ["ssh", "git", "file"] {
let expected = format!("{scheme}://example.com/org/repo");
for password in ["synthetic?value", "synthetic#value"] {
let remote = format!("{scheme}://user:{password}@example.com/org/repo");
let sanitized = SanitizedGitUrl::try_from(remote.as_str()).expect("parse remote URL");
assert_eq!(sanitized.as_str(), expected);
}
}
}
/// Tokens placed in the username field are credentials too.
#[test]
fn removes_https_username_without_password() {