Skip to content

Commit e53e763

Browse files
runningcodeclaudeszokeasaurusrex
authored
feat(build): preserve repository name case for build upload (#2777)
Why make this change? Github is not case sensitive to repository names but our database is sensitive. This means that if we look up a repository name with different casing, we won’t get a match on the database. We decided to make the change to the CLI because if there were ever a provider that did use case sensitive repository names then we would not be able to support it with the given database. See linked Linear issue for the discussion. This PR adds a parameterized version of the `parse` function for use in the `build upload` feature so that we don’t affect other features that use the `parse` function. Two alternate approaches are possible 1. remove the explicit lowercasing in the parse function but I wasn’t sure how that would affect other features. 2. implement a separate `parse_preserve_case` function without adding the extra parameter to the existing function but that approach would yield too much duplicate code. Fixes EME-312 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com>
1 parent b259595 commit e53e763

File tree

2 files changed

+68
-15
lines changed

2 files changed

+68
-15
lines changed

src/commands/build/upload.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ use crate::utils::fs::TempFile;
2525
use crate::utils::progress::ProgressBar;
2626
use crate::utils::vcs::{
2727
self, get_github_base_ref, get_github_pr_number, get_provider_from_remote,
28-
get_repo_from_remote, git_repo_base_ref, git_repo_base_repo_name, git_repo_head_ref,
29-
git_repo_remote_url,
28+
get_repo_from_remote_preserve_case, git_repo_base_ref, git_repo_base_repo_name_preserve_case,
29+
git_repo_head_ref, git_repo_remote_url,
3030
};
3131

3232
pub fn make_command(command: Command) -> Command {
@@ -141,7 +141,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
141141
.or_else(|| {
142142
remote_url
143143
.as_ref()
144-
.map(|url| get_repo_from_remote(url))
144+
.map(|url| get_repo_from_remote_preserve_case(url))
145145
.map(Cow::Owned)
146146
});
147147

@@ -202,7 +202,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
202202
.or_else(|| {
203203
// Try to get the base repo name from the VCS if not provided
204204
repo_ref
205-
.and_then(|r| match git_repo_base_repo_name(r) {
205+
.and_then(|r| match git_repo_base_repo_name_preserve_case(r) {
206206
Ok(Some(base_repo_name)) => {
207207
debug!("Found base repository name: {}", base_repo_name);
208208
Some(base_repo_name)

src/utils/vcs.rs

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ fn strip_git_suffix(s: &str) -> &str {
108108

109109
impl VcsUrl {
110110
pub fn parse(url: &str) -> VcsUrl {
111+
Self::parse_preserve_case(url).into_lowercase()
112+
}
113+
114+
pub fn parse_preserve_case(url: &str) -> VcsUrl {
111115
lazy_static! {
112116
static ref GIT_URL_RE: Regex =
113117
Regex::new(r"^(?:(?:git\+)?(?:git|ssh|https?))://(?:[^@]+@)?([^/]+)/(.+)$")
@@ -129,6 +133,11 @@ impl VcsUrl {
129133
}
130134
}
131135

136+
fn into_lowercase(mut self) -> VcsUrl {
137+
self.id = self.id.to_lowercase();
138+
self
139+
}
140+
132141
fn from_git_parts(host: &str, path: &str) -> VcsUrl {
133142
// Azure Devops has multiple domains and multiple URL styles for the
134143
// various different API versions.
@@ -157,13 +166,13 @@ impl VcsUrl {
157166
if let Some(caps) = VS_GIT_PATH_RE.captures(path) {
158167
return VcsUrl {
159168
provider: host.to_lowercase(),
160-
id: format!("{}/{}", username.to_lowercase(), &caps[1].to_lowercase()),
169+
id: format!("{username}/{}", &caps[1]),
161170
};
162171
}
163172
if let Some(caps) = VS_TRAILING_GIT_PATH_RE.captures(path) {
164173
return VcsUrl {
165174
provider: host.to_lowercase(),
166-
id: caps[1].to_lowercase(),
175+
id: caps[1].to_string(),
167176
};
168177
}
169178
}
@@ -173,13 +182,13 @@ impl VcsUrl {
173182
if let Some(caps) = AZUREDEV_VERSION_PATH_RE.captures(path) {
174183
return VcsUrl {
175184
provider: hostname.into(),
176-
id: format!("{}/{}", &caps[1].to_lowercase(), &caps[2].to_lowercase()),
185+
id: format!("{}/{}", &caps[1], &caps[2]),
177186
};
178187
}
179188
if let Some(caps) = VS_TRAILING_GIT_PATH_RE.captures(path) {
180189
return VcsUrl {
181190
provider: hostname.to_lowercase(),
182-
id: caps[1].to_lowercase(),
191+
id: caps[1].to_string(),
183192
};
184193
}
185194
}
@@ -196,13 +205,13 @@ impl VcsUrl {
196205
if let Some(caps) = BITBUCKET_SERVER_PATH_RE.captures(path) {
197206
return VcsUrl {
198207
provider: host.to_lowercase(),
199-
id: format!("{}/{}", &caps[1].to_lowercase(), &caps[2].to_lowercase()),
208+
id: format!("{}/{}", &caps[1], &caps[2]),
200209
};
201210
}
202211

203212
VcsUrl {
204213
provider: host.to_lowercase(),
205-
id: strip_git_suffix(path).to_lowercase(),
214+
id: strip_git_suffix(path).to_owned(),
206215
}
207216
}
208217
}
@@ -221,6 +230,13 @@ pub fn get_repo_from_remote(repo: &str) -> String {
221230
obj.id
222231
}
223232

233+
/// Like get_repo_from_remote but preserves the original case of the repository name.
234+
/// This is used specifically for build upload where case preservation is important.
235+
pub fn get_repo_from_remote_preserve_case(repo: &str) -> String {
236+
let obj = VcsUrl::parse_preserve_case(repo);
237+
obj.id
238+
}
239+
224240
pub fn get_provider_from_remote(remote: &str) -> String {
225241
let obj = VcsUrl::parse(remote);
226242
extract_provider_name(&obj.provider).to_owned()
@@ -288,10 +304,9 @@ fn find_merge_base_ref(
288304
Ok(merge_base_sha)
289305
}
290306

291-
/// Attempts to get the base repository name from git remotes.
292-
/// Prefers "upstream" remote if it exists, then "origin", otherwise uses the first available remote.
293-
/// Returns the base repository name if a remote is found.
294-
pub fn git_repo_base_repo_name(repo: &git2::Repository) -> Result<Option<String>> {
307+
/// Like git_repo_base_repo_name but preserves the original case of the repository name.
308+
/// This is used specifically for build upload where case preservation is important.
309+
pub fn git_repo_base_repo_name_preserve_case(repo: &git2::Repository) -> Result<Option<String>> {
295310
let remotes = repo.remotes()?;
296311
let remote_names: Vec<&str> = remotes.iter().flatten().collect();
297312

@@ -312,7 +327,8 @@ pub fn git_repo_base_repo_name(repo: &git2::Repository) -> Result<Option<String>
312327
match git_repo_remote_url(repo, chosen_remote) {
313328
Ok(remote_url) => {
314329
debug!("Found remote '{}': {}", chosen_remote, remote_url);
315-
Ok(Some(get_repo_from_remote(&remote_url)))
330+
let repo_name = get_repo_from_remote_preserve_case(&remote_url);
331+
Ok(Some(repo_name))
316332
}
317333
Err(e) => {
318334
warn!("Could not get URL for remote '{}': {}", chosen_remote, e);
@@ -942,6 +958,43 @@ mod tests {
942958
);
943959
}
944960

961+
#[test]
962+
fn test_get_repo_from_remote_preserve_case() {
963+
// Test that case-preserving function maintains original casing
964+
assert_eq!(
965+
get_repo_from_remote_preserve_case("https://github.com/MyOrg/MyRepo"),
966+
"MyOrg/MyRepo"
967+
);
968+
assert_eq!(
969+
get_repo_from_remote_preserve_case("git@github.com:SentryOrg/SentryRepo.git"),
970+
"SentryOrg/SentryRepo"
971+
);
972+
assert_eq!(
973+
get_repo_from_remote_preserve_case("https://gitlab.com/MyCompany/MyProject"),
974+
"MyCompany/MyProject"
975+
);
976+
assert_eq!(
977+
get_repo_from_remote_preserve_case("git@bitbucket.org:TeamName/ProjectName.git"),
978+
"TeamName/ProjectName"
979+
);
980+
assert_eq!(
981+
get_repo_from_remote_preserve_case("ssh://git@github.com/MyUser/MyRepo.git"),
982+
"MyUser/MyRepo"
983+
);
984+
985+
// Test that regular function still lowercases
986+
assert_eq!(
987+
get_repo_from_remote("https://github.com/MyOrg/MyRepo"),
988+
"myorg/myrepo"
989+
);
990+
991+
// Test edge cases - should fall back to lowercase when regex doesn't match
992+
assert_eq!(
993+
get_repo_from_remote_preserve_case("invalid-url"),
994+
get_repo_from_remote("invalid-url")
995+
);
996+
}
997+
945998
#[test]
946999
fn test_extract_provider_name() {
9471000
// Test basic provider name extraction

0 commit comments

Comments
 (0)