Skip to content
93 changes: 89 additions & 4 deletions src/commands/code_mappings/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'."),
)
}

Expand All @@ -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::<String>("repo");
let explicit_branch = matches.get_one::<String>("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<String> {
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<String> {
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}");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate output when values are inferred from git

Low Severity

When repo or branch is inferred from git, infer_repo_name prints "Inferred repository: ..." and infer_default_branch prints "Inferred default branch: ...", and then execute unconditionally prints "Repository: ..." and "Default branch: ..." with the same values. This produces confusing redundant output to the user showing the same information twice.

Additional Locations (2)
Fix in Cursor Fix in Web

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
}
26 changes: 19 additions & 7 deletions src/utils/vcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,27 +301,39 @@ pub fn git_repo_base_ref(repo: &git2::Repository, remote_name: &str) -> Result<S
})
}

/// 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<Option<String>> {
/// 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<Option<String>> {
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"
} else {
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<Option<String>> {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Arguments:
Options:
-o, --org <ORG> The organization ID or slug.
--repo <REPO> The repository name (e.g. owner/repo). Defaults to the git remote.
--default-branch <BRANCH> The default branch name. [default: main]
--default-branch <BRANCH> The default branch name. Defaults to the git remote HEAD or 'main'.
--header <KEY:VALUE> Custom headers that should be attached to all requests
in key:value format.
-p, --project <PROJECT> The project ID or slug.
Expand Down
Loading