Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 54 additions & 47 deletions src/git/repository/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ struct RepoCache {
is_bare: OnceCell<bool>,
/// Default branch (main, master, etc.)
default_branch: OnceCell<String>,
/// Primary remote name (usually "origin")
primary_remote: OnceCell<String>,
/// Primary remote name (None if no remotes configured)
primary_remote: OnceCell<Option<String>>,
/// Project identifier derived from remote URL
project_identifier: OnceCell<String>,
/// Base path for worktrees (repo root for normal repos, bare repo path for bare)
Expand Down Expand Up @@ -197,22 +197,22 @@ impl Repository {
/// Uses the following strategy:
/// 1. Use git's [`checkout.defaultRemote`][1] config if set and has a URL
/// 2. Otherwise, get the first remote with a configured URL
/// 3. Fall back to "origin" if no remotes exist
/// 3. Return error if no remotes exist
///
/// Result is cached in the shared repo cache (shared across all worktrees).
///
/// [1]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-checkoutdefaultRemote
pub fn primary_remote(&self) -> anyhow::Result<String> {
let cache = get_cache(self.compute_git_common_dir()?);
Ok(cache
cache
.primary_remote
.get_or_init(|| {
// Check git's checkout.defaultRemote config
if let Ok(default_remote) = self.run_command(&["config", "checkout.defaultRemote"])
{
let default_remote = default_remote.trim();
if !default_remote.is_empty() && self.remote_has_url(default_remote) {
return default_remote.to_string();
return Some(default_remote.to_string());
}
}

Expand All @@ -230,9 +230,10 @@ impl Repository {
.map(|(name, _)| name)
});

first_remote.unwrap_or("origin").to_string()
first_remote.map(|s| s.to_string())
})
.clone())
.clone()
.ok_or_else(|| anyhow::anyhow!("No remotes configured"))
}

/// Check if a remote has a URL configured.
Expand Down Expand Up @@ -264,8 +265,10 @@ impl Repository {
return Ok(true);
}

// Try remote branch
let remote = self.primary_remote()?;
// Try remote branch (if remotes exist)
let Ok(remote) = self.primary_remote() else {
return Ok(false);
};
Ok(self
.run_command(&[
"rev-parse",
Expand Down Expand Up @@ -1475,42 +1478,46 @@ impl Repository {
let local_branch_names: HashSet<String> =
local_branches.iter().map(|(n, _)| n.clone()).collect();

// Get remote branches with timestamps
let remote = self.primary_remote().unwrap_or_else(|_| "origin".into());
let remote_ref_path = format!("refs/remotes/{}/", remote);
let remote_prefix = format!("{}/", remote);

let remote_output = self.run_command(&[
"for-each-ref",
"--sort=-committerdate",
"--format=%(refname:lstrip=2)\t%(committerdate:unix)",
&remote_ref_path,
])?;

let remote_head = format!("{}/HEAD", remote);
let remote_branches: Vec<(String, String, i64)> = remote_output
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() == 2 {
let full_name = parts[0];
// Skip <remote>/HEAD
if full_name == remote_head {
return None;
}
// Strip remote prefix to get local name
let local_name = full_name.strip_prefix(&remote_prefix)?;
// Skip if local branch exists (user should use local)
if local_branch_names.contains(local_name) {
return None;
// Get remote branches with timestamps (if remotes exist)
let remote_branches: Vec<(String, String, i64)> = if let Ok(remote) = self.primary_remote()
{
let remote_ref_path = format!("refs/remotes/{}/", remote);
let remote_prefix = format!("{}/", remote);

let remote_output = self.run_command(&[
"for-each-ref",
"--sort=-committerdate",
"--format=%(refname:lstrip=2)\t%(committerdate:unix)",
&remote_ref_path,
])?;

let remote_head = format!("{}/HEAD", remote);
remote_output
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() == 2 {
let full_name = parts[0];
// Skip <remote>/HEAD
if full_name == remote_head {
return None;
}
// Strip remote prefix to get local name
let local_name = full_name.strip_prefix(&remote_prefix)?;
// Skip if local branch exists (user should use local)
if local_branch_names.contains(local_name) {
return None;
}
let timestamp = parts[1].parse().unwrap_or(0);
Some((local_name.to_string(), remote.to_string(), timestamp))
} else {
None
}
let timestamp = parts[1].parse().unwrap_or(0);
Some((local_name.to_string(), remote.to_string(), timestamp))
} else {
None
}
})
.collect();
})
.collect()
} else {
Vec::new()
};

// Build result: worktrees first, then local, then remote
let mut result = Vec::new();
Expand Down Expand Up @@ -1841,9 +1848,9 @@ impl Repository {
.project_identifier
.get_or_try_init(|| {
// Try to get the remote URL first
let remote = self.primary_remote()?;

if let Some(url) = self.remote_url(&remote) {
if let Ok(remote) = self.primary_remote()
&& let Some(url) = self.remote_url(&remote)
{
if let Some(parsed) = GitRemoteUrl::parse(url.trim()) {
return Ok(parsed.project_identifier());
}
Expand Down