diff --git a/src/api/mod.rs b/src/api/mod.rs index 7c1dae0f62..32ec3db8fd 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2525,7 +2525,7 @@ struct LogsResponse { data: Vec, } -/// VCS information for mobile app uploads +/// VCS information for build app uploads #[derive(Debug)] pub struct VcsInfo<'a> { pub head_sha: Option<&'a str>, diff --git a/src/commands/build/upload.rs b/src/commands/build/upload.rs index ee3b6b1bab..901c740dc7 100644 --- a/src/commands/build/upload.rs +++ b/src/commands/build/upload.rs @@ -22,7 +22,7 @@ use crate::utils::fs::TempDir; use crate::utils::fs::TempFile; use crate::utils::progress::ProgressBar; use crate::utils::vcs::{ - self, get_provider_from_remote, get_repo_from_remote, git_repo_remote_url, + self, get_provider_from_remote, get_repo_from_remote, git_repo_head_ref, git_repo_remote_url, }; pub fn make_command(command: Command) -> Command { @@ -106,12 +106,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let cached_remote = config.get_cached_vcs_remote(); // Try to open the git repository and find the remote, but handle errors gracefully. - let (vcs_provider, head_repo_name) = { + let (vcs_provider, head_repo_name, head_ref) = { // Try to open the repo and get the remote URL, but don't fail if not in a repo. let repo = git2::Repository::open_from_env().ok(); - let remote_url = repo.and_then(|repo| git_repo_remote_url(&repo, &cached_remote).ok()); + let repo_ref = repo.as_ref(); + let remote_url = repo_ref.and_then(|repo| git_repo_remote_url(repo, &cached_remote).ok()); - let vcs_provider: Option> = matches + let vcs_provider = matches .get_one("vcs_provider") .map(String::as_str) .map(Cow::Borrowed) @@ -122,7 +123,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .map(Cow::Owned) }); - let head_repo_name: Option> = matches + let head_repo_name = matches .get_one("head_repo_name") .map(String::as_str) .map(Cow::Borrowed) @@ -133,13 +134,37 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .map(Cow::Owned) }); - (vcs_provider, head_repo_name) + let head_ref = matches + .get_one("head_ref") + .map(String::as_str) + .map(Cow::Borrowed) + .or_else(|| { + // Try to get the current ref from the VCS if not provided + // Note: git_repo_head_ref will return an error for detached HEAD states, + // which the error handling converts to None - this prevents sending "HEAD" as a branch name + // In that case, the user will need to provide a valid branch name. + repo_ref + .and_then(|r| match git_repo_head_ref(r) { + Ok(ref_name) => { + debug!("Found current branch reference: {}", ref_name); + Some(ref_name) + } + Err(e) => { + debug!( + "No valid branch reference found (likely detached HEAD): {}", + e + ); + None + } + }) + .map(Cow::Owned) + }); + + (vcs_provider, head_repo_name, head_ref) }; let base_repo_name = matches.get_one("base_repo_name").map(String::as_str); - let base_sha = matches.get_one("base_sha").map(String::as_str); - let head_ref = matches.get_one("head_ref").map(String::as_str); let base_ref = matches.get_one("base_ref").map(String::as_str); let pr_number = matches.get_one::("pr_number"); @@ -203,7 +228,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { vcs_provider: vcs_provider.as_deref(), head_repo_name: head_repo_name.as_deref(), base_repo_name, - head_ref, + head_ref: head_ref.as_deref(), base_ref, pr_number, }; diff --git a/src/utils/vcs.rs b/src/utils/vcs.rs index b640154810..e92d0e5084 100644 --- a/src/utils/vcs.rs +++ b/src/utils/vcs.rs @@ -232,6 +232,23 @@ pub fn git_repo_remote_url( .ok_or_else(|| git2::Error::from_str("No remote URL found")) } +pub fn git_repo_head_ref(repo: &git2::Repository) -> Result { + let head = repo.head()?; + + // Only return a reference name if we're not in a detached HEAD state + // In detached HEAD state, head.shorthand() returns "HEAD" which is not a valid branch name + if head.is_branch() { + head.shorthand() + .map(|s| s.to_owned()) + .ok_or_else(|| anyhow::anyhow!("No HEAD reference found")) + } else { + // In detached HEAD state, return an error to indicate no valid branch reference + Err(anyhow::anyhow!( + "HEAD is detached - no branch reference available" + )) + } +} + fn find_reference_url(repo: &str, repos: &[Repo]) -> Result> { let mut non_git = false; for configured_repo in repos { @@ -881,6 +898,14 @@ fn git_initialize_repo() -> TempDir { .wait() .expect("Failed to wait on `git init`."); + Command::new("git") + .args(["branch", "-M", "main"]) + .current_dir(&dir) + .spawn() + .expect("Failed to execute `git branch`.") + .wait() + .expect("Failed to wait on `git branch`."); + Command::new("git") .args(["config", "--local", "user.name", "test"]) .current_dir(&dir) @@ -1188,3 +1213,34 @@ fn test_generate_patch_ignore_missing() { ".*.timestamp" => "[timestamp]" }); } + +#[test] +fn test_git_repo_head_ref() { + let dir = git_initialize_repo(); + + // Create initial commit + git_create_commit( + dir.path(), + "foo.js", + b"console.log(\"Hello, world!\");", + "\"initial commit\"", + ); + + let repo = git2::Repository::open(dir.path()).expect("Failed"); + + // Test on a branch (should succeed) + let head_ref = git_repo_head_ref(&repo).expect("Should get branch reference"); + assert_eq!(head_ref, "main"); + + // Test in detached HEAD state (should fail) + let head_commit = repo.head().unwrap().target().unwrap(); + repo.set_head_detached(head_commit) + .expect("Failed to detach HEAD"); + + let head_ref_result = git_repo_head_ref(&repo); + assert!(head_ref_result.is_err()); + assert_eq!( + head_ref_result.unwrap_err().to_string(), + "HEAD is detached - no branch reference available" + ); +}