67GIAQEUQG3KUD7YTYNUWK33BKWPFVNT4YPQMZ3RCALOZ2STDLRQC
EEF5WHUIPNJAD3OQ7XOY4VSDOMRSPMLZ7EWGDRMADGGAY2YYB6OQC
H4AU6QRPRDRFW3V7NN5CJ6DHLEUBYGNLRZ5GYV6ULBGRMOPCJQXQC
ZSFJT4SFIAS7WBODRZOFKKG4SVYBC5PC6XY75WYN7CCQ3SMV7IUQC
SXEYMYF7P4RZMZ46WPL4IZUTSQ2ATBWYZX7QNVMS3SGOYXYOHAGQC
6ZPDI7QGISBFIIJWV4J4JGCZKW2OZA57ZEQSP5YXWRFW3J6PGMVAC
L4JXJHWXYNCL4QGJXNKKTOKKTAXKKXBJUUY7HFZGEUZ5A2V5H34QC
LGEJSLTYI7Y2CYC3AN6ECMT3D3MTWCAKZPVQEG5MPM2OBW5FQ46AC
3KRGVQFUWFHPOGZOXVTJYNCM4XBRVYITAEOVPKBSAZ5GZIUO5KVQC
OWJL5HO72US47LCBHUZVH6ONALVEADWMFAMXK5RMDHLXSCCFSFQAC
CCLLB7OIFNFYJZTG3UCI7536TOCWSCSXR67VELSB466R24WLJSDAC
A3RM526Y7LUXNYW4TL56YKQ5GVOK2R5D7JJVTSQ6TT5MEXIR6YAAC
EUZFFJSOWV4PXDFFPDAFBHFUUMOFEU6ST7JH57YYRRR2SEOXLN6QC
ZBNKSYA6PW4DSPC3NCRUZLVHW2GNXMCSDSAGEIKHGHDGGZRBH7ZQC
use std::collections::{BTreeSet, HashSet};
use std::io::Write;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::io;
use std::io::{Stdout, Write};
Delete { remote: String },
Delete {
remote: String,
/// Delete only the specified datum, i.e. database ID or remote name. If this isn't specified and there is at most one of each kind of data linked to a remote, the entire remote is deleted.
#[arg(group = "behavior", short = '1', long = "exact")]
exact: bool,
/// Delete all the linked data of the remote, i.e. all names and all database IDs.
#[arg(group = "behavior", long = "flood")]
flood: bool,
},
id: Option<RemoteId>,
path: SmallString,
id: BTreeSet<RemoteId>,
path: String,
configs: BTreeMap<String, ExtraConfig>,
default: bool,
}
struct RemoteInfos {
data: Vec<RemoteInfo>,
by_id: BTreeMap<RemoteId, usize>,
by_url: BTreeMap<String, usize>,
by_label: BTreeMap<String, usize>,
}
struct ExtraConfig {
headers: BTreeMap<String, String>,
default: bool,
fn get_remote_by_any(
repo: &Repository,
txn: &mut MutTxn<()>,
spec: &str,
) -> Result<Option<RemoteInfo>, anyhow::Error> {
// Try indexing by ID first
if let Some(id) = RemoteId::from_base32(spec.as_bytes()) {
if let Some(r) = txn.get_remote(id)? {
let path = r.lock().path.clone();
/// Aggregates remote information from three sources:
/// - the repo database
/// - named remotes in the configuration
/// - the default remote set in the configuration
fn aggregate_remote_info<T>(repo: &Repository, txn: &T) -> Result<RemoteInfos, anyhow::Error>
where
T: TxnT,
{
let mut data = Vec::new();
let mut by_id = BTreeMap::new();
let mut by_url = BTreeMap::new();
let mut by_label = BTreeMap::new();
let mut by_db_name = BTreeMap::new();
return Ok(Some(RemoteInfo { id: Some(id), path }));
for rc in &repo.config.remotes {
let idx = by_url.get(rc.url()).copied().unwrap_or_else(|| {
data.push(RemoteInfo {
path: rc.url().to_string(),
..Default::default()
});
data.len() - 1
});
if rc.db_uses_name() {
by_db_name.insert(rc.name().to_string(), idx);
by_db_name.insert(rc.url().to_string(), idx);
by_label.insert(rc.name().to_string(), idx);
by_url.insert(rc.url().to_string(), idx);
let d = &mut data[idx];
let headers = match rc {
RemoteConfig::Ssh { .. } => BTreeMap::new(),
RemoteConfig::Http { headers, .. } => headers
.iter()
.map(|(k, v)| {
let v = match v {
RemoteHttpHeader::String(s) => s.clone(),
RemoteHttpHeader::Shell(s) => s.shell.clone(),
};
(k.clone(), v)
})
.collect(),
};
d.configs.insert(
rc.name().to_string(),
ExtraConfig {
headers,
// set later
default: false,
},
);
if path.as_str() == spec {
return Ok(Some(RemoteInfo {
id: Some(*r.id()),
path,
}));
}
let idx = by_db_name
.get(lock.path.as_str())
.copied()
.unwrap_or_else(|| {
data.push(RemoteInfo {
path: lock.path.as_str().to_string(),
..Default::default()
});
by_url.insert(lock.path.as_str().to_string(), data.len() - 1);
data.len() - 1
});
data[idx].id.insert(*r.id());
by_id.insert(*r.id(), idx);
// Otherwise, is it set as the default remote?
if repo.config.default_remote.as_deref() == Some(spec) {
return Ok(Some(RemoteInfo {
id: None,
path: SmallString::from_str(spec),
}));
if let Some(default) = &repo.config.default_remote {
let label_idx = by_label.get(default);
let idx = label_idx
.or_else(|| by_url.get(default))
.copied()
.unwrap_or_else(|| {
data.push(RemoteInfo {
path: default.to_string(),
..Default::default()
});
by_url.insert(default.to_string(), data.len() - 1);
data.len() - 1
});
data[idx].default = true;
if label_idx.is_some() {
for (label, ec) in data[idx].configs.iter_mut() {
if label == default {
ec.default = true;
}
}
}
for r in txn.iter_remotes(&RemoteId::nil())? {
let r = r?;
let lock = r.lock();
for info in remote_infos.data {
let can_collapse = info.configs.len() < 2
|| info.configs.iter().all(|(_, el)| el.headers.is_empty());
writeln!(stdout, "{} {}: {}", flag, r.id(), lock.path.as_str())?;
}
// Under normal circumstances, there should only be at most
// one ID here. However, still do our best to format it in a
// reasonably nice way.
let mut ids = info.id.iter();
write!(stdout, "{} ", flag)?;
if !printed_default {
if let Some(path) = repo.config.default_remote {
writeln!(stdout, "* {:26}: {}", "(none)", path)?;
if let Some(id) = ids.next() {
write!(stdout, "{}", id)?;
} else {
write!(stdout, "{:26}", "(no ID)")?;
}
write!(stdout, ": ")?;
fn write_headers(stdout: &mut Stdout, c: &ExtraConfig) -> io::Result<()> {
if !c.headers.is_empty() {
writeln!(stdout, " Headers:")?;
for (header, value) in &c.headers {
writeln!(stdout, " - {}: {}", header, value)?;
}
}
Ok(())
}
if can_collapse {
for (name, _) in &info.configs {
write!(stdout, "«{}» ", name)?;
}
writeln!(stdout, "{}", info.path)?;
for (_, c) in &info.configs {
write_headers(&mut stdout, c)?;
}
} else {
writeln!(stdout, "{}", info.path)?;
for (name, c) in &info.configs {
let mut flag = ' ';
if c.default {
flag = '*';
}
writeln!(stdout, "{} «{}»", flag, name)?;
write_headers(&mut stdout, c)?;
}
}
if let Some(extra_id) = ids.next() {
write!(stdout, " (also registered as {}", extra_id)?;
while let Some(extra_id) = ids.next() {
write!(stdout, ", {}", extra_id)?;
}
writeln!(stdout, ")")?;
let Some(ri) = get_remote_by_any(&repo, &mut txn, &remote)? else {
bail!("No such remote: {}", remote);
let label_idx = db.by_label.get(&spec);
let remote = RemoteId::from_base32(spec.as_bytes());
let remote_idx = remote.and_then(|r| db.by_id.get(&r));
let url_idx = db.by_url.get(&spec);
let Some(&idx) = label_idx.or(remote_idx).or(url_idx) else {
bail!("No such remote: {}", spec);
if repo.config.default_remote.as_deref() == Some(ri.path.as_str()) {
repo.config.default_remote = None;
fn flood_delete(
exact: bool,
flood: bool,
d: &RemoteInfo,
repo: &mut Repository,
txn: &mut MutTxn<()>,
) -> Result<(), anyhow::Error> {
let mut delete_count = 0;
for id in &d.id {
txn.drop_named_remote(*id)?;
delete_count += 1;
}
if !flood {
// just in case, but these should never trigger because
// of the checks beforehand
assert!(delete_count <= 1);
}
if !exact {
delete_count = 0;
}
for (name, _ec) in &d.configs {
remove_named_repo(repo, name);
delete_count += 1;
}
if !flood {
assert!(delete_count <= 1);
}
if d.default {
repo.config.default_remote = None;
}
Ok(())
}
if flood {
flood_delete(flood, exact, d, &mut repo, &mut txn)?;
} else {
// all the flood_delete calls in this block are ensured to only delete at most one thing
if label_idx.is_some() {
if !exact && d.id.len() <= 1 && d.configs.len() <= 1 {
flood_delete(flood, exact, d, &mut repo, &mut txn)?;
} else {
let idx = repo
.config
.remotes
.iter()
.position(|c| c.name() == spec)
.unwrap();
repo.config.remotes.remove(idx);
if d.configs.get(&spec).unwrap().default {
repo.config.default_remote = None;
}
}
} else if remote_idx.is_some() {
if !exact && d.id.len() <= 1 && d.configs.len() <= 1 {
flood_delete(flood, exact, d, &mut repo, &mut txn)?;
} else {
txn.drop_named_remote(remote.unwrap())?;
}
} else if url_idx.is_some() {
if exact && !d.configs.is_empty() && !d.id.is_empty() {
bail!("Cannot delete '{}' since there is both a named remote and an ID associated with it", spec);
}
if d.configs.len() > 1 {
bail!("Cannot delete '{}' since there are multiple named remotes associated with it", spec);
}
if d.id.len() > 1 {
bail!("Cannot delete '{}' since there are multiple IDs associated with it", spec);
}
flood_delete(flood, exact, d, &mut repo, &mut txn)?;
}