diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index d40d6427c3..aa200bea12 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -2,9 +2,13 @@ use std::fs; use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; -use serde::Deserialize; +use log::debug; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +use crate::config::Config; +use crate::utils::vcs; + +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct CodeMapping { stack_root: String, @@ -30,8 +34,7 @@ pub fn make_command(command: Command) -> Command { Arg::new("default_branch") .long("default-branch") .value_name("BRANCH") - .default_value("main") - .help("The default branch name."), + .help("The default branch name. Defaults to the git remote HEAD or 'main'."), ) } @@ -57,7 +60,89 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } } + // Resolve repo name and default branch, falling back to git inference. + let explicit_repo = matches.get_one::("repo"); + let explicit_branch = matches.get_one::("default_branch"); + + let git_repo = (explicit_repo.is_none() || explicit_branch.is_none()) + .then(|| git2::Repository::open_from_env().ok()) + .flatten(); + let remote_name = git_repo.as_ref().and_then(resolve_git_remote); + + let repo_name = if let Some(r) = explicit_repo { + r.to_owned() + } else { + infer_repo_name(git_repo.as_ref(), remote_name.as_deref())? + }; + + let default_branch = if let Some(b) = explicit_branch { + b.to_owned() + } else { + infer_default_branch(git_repo.as_ref(), remote_name.as_deref()) + }; + println!("Found {} code mapping(s) in {path}", mappings.len()); + println!("Repository: {repo_name}"); + println!("Default branch: {default_branch}"); Ok(()) } + +/// Finds the best git remote name. Prefers the configured VCS remote +/// (SENTRY_VCS_REMOTE / ini), then falls back to upstream > origin > first. +fn resolve_git_remote(repo: &git2::Repository) -> Option { + let config = Config::current(); + let configured_remote = config.get_cached_vcs_remote(); + if vcs::git_repo_remote_url(repo, &configured_remote).is_ok() { + debug!("Using configured VCS remote: {configured_remote}"); + return Some(configured_remote); + } + match vcs::find_best_remote(repo) { + Ok(Some(best)) => { + debug!("Configured remote '{configured_remote}' not found, using: {best}"); + Some(best) + } + _ => None, + } +} + +/// Infers the repository name (e.g. "owner/repo") from the git remote URL. +fn infer_repo_name( + git_repo: Option<&git2::Repository>, + remote_name: Option<&str>, +) -> Result { + let git_repo = git_repo.ok_or_else(|| { + anyhow::anyhow!("Could not open git repository. Use --repo to specify manually.") + })?; + let remote_name = remote_name.ok_or_else(|| { + anyhow::anyhow!("No remotes found in the git repository. Use --repo to specify manually.") + })?; + let remote_url = vcs::git_repo_remote_url(git_repo, remote_name)?; + debug!("Found remote '{remote_name}': {remote_url}"); + let inferred = vcs::get_repo_from_remote_preserve_case(&remote_url); + if inferred.is_empty() { + bail!("Could not parse repository name from remote URL: {remote_url}"); + } + println!("Inferred repository: {inferred}"); + Ok(inferred) +} + +/// Infers the default branch from the git remote HEAD, falling back to "main". +fn infer_default_branch(git_repo: Option<&git2::Repository>, remote_name: Option<&str>) -> String { + let inferred = git_repo + .zip(remote_name) + .and_then(|(repo, name)| { + vcs::git_repo_base_ref(repo, name) + .map_err(|e| { + debug!("Could not infer default branch from remote: {e}"); + e + }) + .ok() + }) + .unwrap_or_else(|| { + debug!("No git repo or remote available, falling back to 'main'"); + "main".to_owned() + }); + println!("Inferred default branch: {inferred}"); + inferred +} diff --git a/src/utils/vcs.rs b/src/utils/vcs.rs index 948c35a0f9..2fd89b372d 100644 --- a/src/utils/vcs.rs +++ b/src/utils/vcs.rs @@ -301,19 +301,17 @@ pub fn git_repo_base_ref(repo: &git2::Repository, remote_name: &str) -> Result Result> { +/// Finds the best remote in a git repository. +/// Prefers "upstream" if it exists, then "origin", otherwise uses the first remote. +pub fn find_best_remote(repo: &git2::Repository) -> Result> { let remotes = repo.remotes()?; let remote_names: Vec<&str> = remotes.iter().flatten().collect(); if remote_names.is_empty() { - warn!("No remotes found in repository"); return Ok(None); } - // Prefer "upstream" if it exists, then "origin", otherwise use the first one - let chosen_remote = if remote_names.contains(&"upstream") { + let chosen = if remote_names.contains(&"upstream") { "upstream" } else if remote_names.contains(&"origin") { "origin" @@ -321,7 +319,21 @@ pub fn git_repo_base_repo_name_preserve_case(repo: &git2::Repository) -> Result< remote_names[0] }; - match git_repo_remote_url(repo, chosen_remote) { + Ok(Some(chosen.to_owned())) +} + +/// Like git_repo_base_repo_name but preserves the original case of the repository name. +/// This is used specifically for build upload where case preservation is important. +pub fn git_repo_base_repo_name_preserve_case(repo: &git2::Repository) -> Result> { + let chosen_remote = match find_best_remote(repo)? { + Some(remote) => remote, + None => { + warn!("No remotes found in repository"); + return Ok(None); + } + }; + + match git_repo_remote_url(repo, &chosen_remote) { Ok(remote_url) => { debug!("Found remote '{chosen_remote}': {remote_url}"); let repo_name = get_repo_from_remote_preserve_case(&remote_url); diff --git a/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd b/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd index 01033d6915..7eec72d0c2 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd @@ -13,7 +13,7 @@ Arguments: Options: -o, --org The organization ID or slug. --repo The repository name (e.g. owner/repo). Defaults to the git remote. - --default-branch The default branch name. [default: main] + --default-branch The default branch name. Defaults to the git remote HEAD or 'main'. --header Custom headers that should be attached to all requests in key:value format. -p, --project The project ID or slug.