From 9f73bc0f9274743eb4472ec2013ac1f21c88a1ba Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 11 Mar 2026 23:04:18 +0100 Subject: [PATCH 1/8] feat(cli): Add `code-mappings upload` command scaffold with file parsing Add a new `code-mappings` subcommand group with an `upload` subcommand that reads and validates a JSON file of code mappings. This is the first step toward CLI support for bulk code mapping uploads to Sentry. --- src/commands/code_mappings/upload.rs | 11 +++++------ .../_cases/code_mappings/code-mappings-help.trycmd | 4 +--- .../code_mappings/code-mappings-no-subcommand.trycmd | 4 +--- .../code_mappings/code-mappings-upload-help.trycmd | 4 +--- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index d40d6427c3..f0141c7d42 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -2,9 +2,9 @@ use std::fs; use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct CodeMapping { stack_root: String, @@ -13,7 +13,7 @@ struct CodeMapping { pub fn make_command(command: Command) -> Command { command - .about("Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root (e.g. com/example/module) with the corresponding source path in your repository (e.g. modules/module/src/main/java/com/example/module).") + .about("Upload code mappings for a project from a JSON file.") .arg( Arg::new("path") .value_name("PATH") @@ -36,9 +36,8 @@ pub fn make_command(command: Command) -> Command { } pub fn execute(matches: &ArgMatches) -> Result<()> { - let path = matches - .get_one::("path") - .expect("path is a required argument"); + #[expect(clippy::unwrap_used, reason = "path is a required argument")] + let path = matches.get_one::("path").unwrap(); let data = fs::read(path).with_context(|| format!("Failed to read mappings file '{path}'"))?; let mappings: Vec = diff --git a/tests/integration/_cases/code_mappings/code-mappings-help.trycmd b/tests/integration/_cases/code_mappings/code-mappings-help.trycmd index 759ddda529..9ad67263a9 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-help.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-help.trycmd @@ -7,9 +7,7 @@ repository, enabling source context and code linking in Sentry. Usage: sentry-cli[EXE] code-mappings [OPTIONS] Commands: - upload Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root - (e.g. com/example/module) with the corresponding source path in your repository (e.g. - modules/module/src/main/java/com/example/module). + upload Upload code mappings for a project from a JSON file. help Print this message or the help of the given subcommand(s) Options: diff --git a/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd b/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd index 64f98301c4..d5010c8c83 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd @@ -7,9 +7,7 @@ repository, enabling source context and code linking in Sentry. Usage: sentry-cli[EXE] code-mappings [OPTIONS] Commands: - upload Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root - (e.g. com/example/module) with the corresponding source path in your repository (e.g. - modules/module/src/main/java/com/example/module). + upload Upload code mappings for a project from a JSON file. help Print this message or the help of the given subcommand(s) Options: 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..cf46abe2c2 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd @@ -1,9 +1,7 @@ ``` $ sentry-cli code-mappings upload --help ? success -Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root (e.g. -com/example/module) with the corresponding source path in your repository (e.g. -modules/module/src/main/java/com/example/module). +Upload code mappings for a project from a JSON file. Usage: sentry-cli[EXE] code-mappings upload [OPTIONS] From dfeb139a11bca39fa2f76fa0f859d563ac71fd5a Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 18 Mar 2026 17:52:30 +0100 Subject: [PATCH 2/8] fix(code-mappings): Address PR feedback on help text and unwrap usage Add descriptive help text explaining what code mappings are and how they work. Replace unwrap with expect for the required path argument. Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 7 ++++--- .../_cases/code_mappings/code-mappings-help.trycmd | 4 +++- .../code_mappings/code-mappings-no-subcommand.trycmd | 4 +++- .../_cases/code_mappings/code-mappings-upload-help.trycmd | 4 +++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index f0141c7d42..3e6582f27e 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -13,7 +13,7 @@ struct CodeMapping { pub fn make_command(command: Command) -> Command { command - .about("Upload code mappings for a project from a JSON file.") + .about("Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root (e.g. com/example/module) with the corresponding source path in your repository (e.g. modules/module/src/main/java/com/example/module).") .arg( Arg::new("path") .value_name("PATH") @@ -36,8 +36,9 @@ pub fn make_command(command: Command) -> Command { } pub fn execute(matches: &ArgMatches) -> Result<()> { - #[expect(clippy::unwrap_used, reason = "path is a required argument")] - let path = matches.get_one::("path").unwrap(); + let path = matches + .get_one::("path") + .expect("path is a required argument"); let data = fs::read(path).with_context(|| format!("Failed to read mappings file '{path}'"))?; let mappings: Vec = diff --git a/tests/integration/_cases/code_mappings/code-mappings-help.trycmd b/tests/integration/_cases/code_mappings/code-mappings-help.trycmd index 9ad67263a9..759ddda529 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-help.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-help.trycmd @@ -7,7 +7,9 @@ repository, enabling source context and code linking in Sentry. Usage: sentry-cli[EXE] code-mappings [OPTIONS] Commands: - upload Upload code mappings for a project from a JSON file. + upload Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root + (e.g. com/example/module) with the corresponding source path in your repository (e.g. + modules/module/src/main/java/com/example/module). help Print this message or the help of the given subcommand(s) Options: diff --git a/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd b/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd index d5010c8c83..64f98301c4 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-no-subcommand.trycmd @@ -7,7 +7,9 @@ repository, enabling source context and code linking in Sentry. Usage: sentry-cli[EXE] code-mappings [OPTIONS] Commands: - upload Upload code mappings for a project from a JSON file. + upload Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root + (e.g. com/example/module) with the corresponding source path in your repository (e.g. + modules/module/src/main/java/com/example/module). help Print this message or the help of the given subcommand(s) Options: 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 cf46abe2c2..01033d6915 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-upload-help.trycmd @@ -1,7 +1,9 @@ ``` $ sentry-cli code-mappings upload --help ? success -Upload code mappings for a project from a JSON file. +Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root (e.g. +com/example/module) with the corresponding source path in your repository (e.g. +modules/module/src/main/java/com/example/module). Usage: sentry-cli[EXE] code-mappings upload [OPTIONS] From 27a3b3a95bb454c0fa2c97cd2d0b079dd21ff1ae Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 11 Mar 2026 23:35:04 +0100 Subject: [PATCH 3/8] feat(cli): Add git inference for repo and branch in code-mappings upload Automatically detect repository name and default branch from the local git repo when --repo or --default-branch are not provided. Respects SENTRY_VCS_REMOTE config, falling back to best-effort remote detection. Extract find_best_remote() into vcs.rs to deduplicate remote selection logic shared with git_repo_base_repo_name_preserve_case(). --- src/commands/code_mappings/upload.rs | 71 ++++++++++++++++++- src/utils/vcs.rs | 26 +++++-- .../code-mappings-upload-help.trycmd | 2 +- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 3e6582f27e..402d62beee 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -2,8 +2,12 @@ use std::fs; use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; +use log::debug; use serde::{Deserialize, Serialize}; +use crate::config::Config; +use crate::utils::vcs; + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct CodeMapping { @@ -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,71 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } } + // Resolve repo name and default branch + let explicit_repo = matches.get_one::("repo"); + let explicit_branch = matches.get_one::("default_branch"); + + let (repo_name, default_branch) = match (explicit_repo, explicit_branch) { + (Some(r), Some(b)) => (r.to_owned(), b.to_owned()), + _ => { + let git_repo = git2::Repository::open_from_env().map_err(|e| { + anyhow::anyhow!( + "Could not open git repository: {e}. \ + Use --repo and --default-branch to specify manually." + ) + })?; + // Prefer explicit config (SENTRY_VCS_REMOTE / ini), then inspect + // the repo for the best remote (upstream > origin > first). + let config = Config::current(); + let configured_remote = config.get_cached_vcs_remote(); + let remote_name = + if vcs::git_repo_remote_url(&git_repo, &configured_remote).is_ok() { + debug!("Using configured VCS remote: {configured_remote}"); + configured_remote + } else if let Some(best) = vcs::find_best_remote(&git_repo)? { + debug!("Configured remote '{configured_remote}' not found, using: {best}"); + best + } else { + bail!( + "No remotes found in the git repository. \ + Use --repo and --default-branch to specify manually." + ); + }; + + let repo_name = match explicit_repo { + Some(r) => r.to_owned(), + None => { + 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(&remote_url); + if inferred.is_empty() { + bail!("Could not parse repository name from remote URL: {remote_url}"); + } + println!("Inferred repository: {inferred}"); + inferred + } + }; + + let default_branch = match explicit_branch { + Some(b) => b.to_owned(), + None => { + let inferred = + vcs::git_repo_base_ref(&git_repo, &remote_name).unwrap_or_else(|e| { + debug!("Could not infer default branch, falling back to 'main': {e}"); + "main".to_owned() + }); + println!("Inferred default branch: {inferred}"); + inferred + } + }; + + (repo_name, default_branch) + } + }; + println!("Found {} code mapping(s) in {path}", mappings.len()); + println!("Repository: {repo_name}"); + println!("Default branch: {default_branch}"); Ok(()) } 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. From b6e9157bec0f548a3de0958d8ece2f07ba578b35 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 12 Mar 2026 12:25:25 +0100 Subject: [PATCH 4/8] style: Fix formatting in code-mappings upload --- src/commands/code_mappings/upload.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 402d62beee..321debbb02 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -77,19 +77,18 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { // the repo for the best remote (upstream > origin > first). let config = Config::current(); let configured_remote = config.get_cached_vcs_remote(); - let remote_name = - if vcs::git_repo_remote_url(&git_repo, &configured_remote).is_ok() { - debug!("Using configured VCS remote: {configured_remote}"); - configured_remote - } else if let Some(best) = vcs::find_best_remote(&git_repo)? { - debug!("Configured remote '{configured_remote}' not found, using: {best}"); - best - } else { - bail!( - "No remotes found in the git repository. \ + let remote_name = if vcs::git_repo_remote_url(&git_repo, &configured_remote).is_ok() { + debug!("Using configured VCS remote: {configured_remote}"); + configured_remote + } else if let Some(best) = vcs::find_best_remote(&git_repo)? { + debug!("Configured remote '{configured_remote}' not found, using: {best}"); + best + } else { + bail!( + "No remotes found in the git repository. \ Use --repo and --default-branch to specify manually." - ); - }; + ); + }; let repo_name = match explicit_repo { Some(r) => r.to_owned(), From 12d1a2b4d35e6643f3e4654b5364862373b064fb Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 18 Mar 2026 18:02:45 +0100 Subject: [PATCH 5/8] fix(code-mappings): Allow branch fallback without git remotes When --repo is provided but --default-branch is not, the code no longer requires a git remote to be present. Branch inference gracefully falls back to 'main' when no git repo or remote is available. Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 73 +++++++++++++++++++--------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 321debbb02..01a219db62 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -67,33 +67,46 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let (repo_name, default_branch) = match (explicit_repo, explicit_branch) { (Some(r), Some(b)) => (r.to_owned(), b.to_owned()), _ => { - let git_repo = git2::Repository::open_from_env().map_err(|e| { - anyhow::anyhow!( - "Could not open git repository: {e}. \ - Use --repo and --default-branch to specify manually." - ) - })?; + let git_repo = git2::Repository::open_from_env(); + + // Resolve the best remote name when we have a git repo. // Prefer explicit config (SENTRY_VCS_REMOTE / ini), then inspect // the repo for the best remote (upstream > origin > first). - let config = Config::current(); - let configured_remote = config.get_cached_vcs_remote(); - let remote_name = if vcs::git_repo_remote_url(&git_repo, &configured_remote).is_ok() { - debug!("Using configured VCS remote: {configured_remote}"); - configured_remote - } else if let Some(best) = vcs::find_best_remote(&git_repo)? { - debug!("Configured remote '{configured_remote}' not found, using: {best}"); - best - } else { - bail!( - "No remotes found in the git repository. \ - Use --repo and --default-branch to specify manually." - ); - }; + let remote_name = git_repo.as_ref().ok().and_then(|repo| { + 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}"); + Some(configured_remote) + } else { + match vcs::find_best_remote(repo) { + Ok(Some(best)) => { + debug!( + "Configured remote '{configured_remote}' not found, using: {best}" + ); + Some(best) + } + _ => None, + } + } + }); let repo_name = match explicit_repo { Some(r) => r.to_owned(), None => { - let remote_url = vcs::git_repo_remote_url(&git_repo, &remote_name)?; + let git_repo = git_repo.as_ref().map_err(|e| { + anyhow::anyhow!( + "Could not open git repository: {e}. \ + Use --repo to specify manually." + ) + })?; + let remote_name = remote_name.as_deref().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(&remote_url); if inferred.is_empty() { @@ -107,9 +120,21 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let default_branch = match explicit_branch { Some(b) => b.to_owned(), None => { - let inferred = - vcs::git_repo_base_ref(&git_repo, &remote_name).unwrap_or_else(|e| { - debug!("Could not infer default branch, falling back to 'main': {e}"); + let inferred = git_repo + .as_ref() + .ok() + .and_then(|repo| { + remote_name.as_deref().and_then(|name| { + vcs::git_repo_base_ref(repo, name) + .map(Some) + .unwrap_or_else(|e| { + debug!("Could not infer default branch from remote: {e}"); + None + }) + }) + }) + .unwrap_or_else(|| { + debug!("No git repo or remote available, falling back to 'main'"); "main".to_owned() }); println!("Inferred default branch: {inferred}"); From 6dae115dab185479ff93931616a99c4adee76f40 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 18 Mar 2026 18:41:27 +0100 Subject: [PATCH 6/8] fix(code-mappings): Preserve case when inferring repo name from remote Use get_repo_from_remote_preserve_case instead of get_repo_from_remote to avoid lowercasing the repository name, which would cause mismatches with Sentry's case-sensitive API. Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 01a219db62..7daed0154c 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -108,7 +108,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { })?; 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(&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}"); } From 62b9fbfa46c1e353c9e597cffd267e9e8e44260e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 20 Mar 2026 22:49:38 +0100 Subject: [PATCH 7/8] ref(code-mappings): Flatten git inference into helper functions Replace the deeply nested match with two independent lookups for repo_name and default_branch. Extract resolve_git_remote, infer_repo_name, and infer_default_branch as utility functions. Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 156 +++++++++++++-------------- 1 file changed, 75 insertions(+), 81 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 7daed0154c..34d7935443 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -60,90 +60,25 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } } - // Resolve repo name and default branch + // 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 (repo_name, default_branch) = match (explicit_repo, explicit_branch) { - (Some(r), Some(b)) => (r.to_owned(), b.to_owned()), - _ => { - let git_repo = git2::Repository::open_from_env(); - - // Resolve the best remote name when we have a git repo. - // Prefer explicit config (SENTRY_VCS_REMOTE / ini), then inspect - // the repo for the best remote (upstream > origin > first). - let remote_name = git_repo.as_ref().ok().and_then(|repo| { - 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}"); - Some(configured_remote) - } else { - match vcs::find_best_remote(repo) { - Ok(Some(best)) => { - debug!( - "Configured remote '{configured_remote}' not found, using: {best}" - ); - Some(best) - } - _ => None, - } - } - }); - - let repo_name = match explicit_repo { - Some(r) => r.to_owned(), - None => { - let git_repo = git_repo.as_ref().map_err(|e| { - anyhow::anyhow!( - "Could not open git repository: {e}. \ - Use --repo to specify manually." - ) - })?; - let remote_name = remote_name.as_deref().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}"); - inferred - } - }; - - let default_branch = match explicit_branch { - Some(b) => b.to_owned(), - None => { - let inferred = git_repo - .as_ref() - .ok() - .and_then(|repo| { - remote_name.as_deref().and_then(|name| { - vcs::git_repo_base_ref(repo, name) - .map(Some) - .unwrap_or_else(|e| { - debug!("Could not infer default branch from remote: {e}"); - None - }) - }) - }) - .unwrap_or_else(|| { - debug!("No git repo or remote available, falling back to 'main'"); - "main".to_owned() - }); - println!("Inferred default branch: {inferred}"); - inferred - } - }; - - (repo_name, 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(|repo| resolve_git_remote(repo)); + + 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()); @@ -152,3 +87,62 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { 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 +} From 44ef01d03ac2071a9136a60c2d01888272618ab0 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 20 Mar 2026 22:52:32 +0100 Subject: [PATCH 8/8] style(code-mappings): Remove redundant closure to fix clippy warning Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 34d7935443..aa200bea12 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -67,7 +67,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { 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(|repo| resolve_git_remote(repo)); + let remote_name = git_repo.as_ref().and_then(resolve_git_remote); let repo_name = if let Some(r) = explicit_repo { r.to_owned()