use super::*;
pub(crate) use mj_core::repository::RepositoryIdentity;
pub(super) fn root_identity(root: &Path) -> Result<RepositoryIdentity> {
mj_core::repository::local_identity(
root,
&mj_core::targets::CancellableProcessExecutor::with_timeout(Duration::from_secs(8)),
)
}
pub(crate) fn configured_repository_identity(
repository: &ProjectRepository,
) -> Result<Option<RepositoryIdentity>> {
if let Some(source) = repository.github.as_deref() {
return Ok(RepositoryIdentity::from_remote(source));
}
repository.local.as_deref().map(root_identity).transpose()
}
pub(crate) fn configured_repository_identity_for_lookup(
bundle_id: &str,
repository: &ProjectRepository,
) -> Option<RepositoryIdentity> {
match configured_repository_identity(repository) {
Ok(identity) => identity,
Err(error) => {
let path = repository
.local
.as_deref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "<not configured>".to_owned());
tracing::warn!(
bundle_id = %bundle_id,
repository_id = %repository.id,
path = %path,
error = %error,
"skipping configured repository whose identity cannot be read"
);
None
}
}
}
pub fn resolve_bundle(
config: &Config,
cwd: &Path,
targets: &SessionEditTargets,
requested_bundle: Option<&str>,
) -> Result<BundleResolution> {
let cwd_root = git_root_for_path(cwd)?.context("session cwd is not in a Git worktree")?;
let cwd_root = main_worktree_root(&cwd_root)?;
let primary_identity = root_identity(&cwd_root)?;
let detected = targets
.git_roots
.iter()
.map(|root| root_identity(root))
.collect::<Result<BTreeSet<_>>>()?;
if let Some(bundle_id) = requested_bundle {
let bundle = config
.bundles
.get(bundle_id)
.with_context(|| format!("unknown bundle {bundle_id:?}"))?;
ensure!(
bundle_matches(bundle_id, bundle, &detected, &primary_identity)?,
"bundle {bundle_id:?} does not exactly match the session's edited Git roots and cwd primary repository"
);
return Ok(BundleResolution::Existing(bundle_id.to_owned()));
}
for (id, bundle) in &config.bundles {
if bundle_matches(id, bundle, &detected, &primary_identity)? {
return Ok(BundleResolution::Existing(id.clone()));
}
}
let primary_name = cwd_root
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("repository");
let bundle_id = unique_bundle_id(config, &setup_style_id(primary_name));
let mut used_ids = BTreeSet::new();
let mut repositories = Vec::new();
let mut primary_repo = None;
let mut roots = targets
.git_roots
.iter()
.map(|root| main_worktree_root(root))
.collect::<Result<Vec<_>>>()?;
roots.sort_by_key(|root| root != &cwd_root);
let mut used_identities = BTreeSet::new();
for root in roots {
if !used_identities.insert(root_identity(&root)?) {
continue;
}
let base = setup_style_id(
root.file_name()
.and_then(|name| name.to_str())
.unwrap_or("repository"),
);
let mut id = base.clone();
for suffix in 2_u32.. {
if used_ids.insert(id.clone()) {
break;
}
id = format!("{base}-{suffix}");
}
if root == cwd_root {
primary_repo = Some(id.clone());
}
repositories.push(ProjectRepository {
id: id.clone(),
local: Some(root),
github: None,
destination: PathBuf::from(id),
git_ref: None,
});
}
Ok(BundleResolution::Synthesized {
id: bundle_id,
bundle: ProjectBundle {
primary_repo: primary_repo.context("detected roots omitted the cwd repository")?,
repositories,
},
})
}
pub(crate) fn bundle_matches(
bundle_id: &str,
bundle: &ProjectBundle,
detected: &BTreeSet<RepositoryIdentity>,
primary: &RepositoryIdentity,
) -> Result<bool> {
let identity_results = bundle
.repositories
.iter()
.map(|repository| configured_repository_identity_for_lookup(bundle_id, repository))
.collect::<Vec<_>>();
if identity_results.iter().any(Option::is_none) {
return Ok(false);
}
let identities = identity_results.into_iter().flatten().collect::<Vec<_>>();
let identity_set = identities.iter().cloned().collect::<BTreeSet<_>>();
let Some(primary_index) = bundle
.repositories
.iter()
.position(|repository| repository.id == bundle.primary_repo)
else {
return Ok(false);
};
Ok(identity_set.len() == bundle.repositories.len()
&& &identity_set == detected
&& identities.get(primary_index) == Some(primary))
}
pub fn configured_bundle_for_origin(config: &Config, origin: &GithubRepository) -> Option<String> {
config.bundles.iter().find_map(|(id, bundle)| {
if bundle.repositories.len() != 1 {
return None;
}
let primary = bundle.primary()?;
let configured = github_repository_from_origin(primary.github.as_deref()?)?;
same_github_repository(&configured, origin).then(|| id.clone())
})
}
pub fn configured_bundle_for_local(config: &Config, local: &Path) -> Option<String> {
let local = mj_core::local_git::canonical_repository(local).ok()?;
config.bundles.iter().find_map(|(id, bundle)| {
if bundle.repositories.len() != 1 {
return None;
}
let configured = bundle.primary()?.local.as_ref()?;
let configured = mj_core::local_git::canonical_repository(configured).ok()?;
(configured == local).then(|| id.clone())
})
}
pub(super) fn same_github_repository(left: &GithubRepository, right: &GithubRepository) -> bool {
left.owner.eq_ignore_ascii_case(&right.owner)
&& left.repository.eq_ignore_ascii_case(&right.repository)
}
pub(crate) fn setup_style_id(value: &str) -> String {
let mut id = value
.chars()
.filter(|character| {
character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')
})
.take(64)
.collect::<String>();
if id.is_empty() || matches!(id.as_str(), "." | "..") {
id = "repository".into();
}
id
}
pub(crate) fn unique_bundle_id(config: &Config, base: &str) -> String {
if !config.bundles.contains_key(base) {
return base.into();
}
let base = format!("import-{base}");
if !config.bundles.contains_key(&base) {
return base;
}
for suffix in 2_u32.. {
let candidate = format!("{base}-{suffix}");
if !config.bundles.contains_key(&candidate) {
return candidate;
}
}
unreachable!("u32 bundle suffixes are finite")
}