1#[must_use]
24pub fn repository_name(name: &str) -> String {
25 if name.ends_with(".git") {
26 name.to_string()
27 } else {
28 format!("{name}.git")
29 }
30}
31
32#[must_use]
34pub fn repositories(layout: &crate::serve::Layout) -> Vec<String> {
35 std::fs::read_to_string(layout.state.join("repos.list"))
36 .unwrap_or_default()
37 .lines()
38 .map(str::trim)
39 .filter(|l| !l.is_empty() && !l.starts_with('#'))
40 .map(str::to_string)
41 .collect()
42}
43
44pub fn add(
55 layout: &crate::serve::Layout,
56 repo: &str,
57 name: &str,
58 url: &str,
59) -> Result<bool, String> {
60 let repo = repository_name(repo);
61 let bare = layout.repos.join(&repo);
62 if !bare.join("HEAD").is_file() {
63 return Err(format!(
64 "no repository {repo} under {}\n\n what is here: choir repo follower list",
65 layout.repos.display()
66 ));
67 }
68 if name.is_empty() || name.starts_with('-') || name.contains(['/', ' ']) {
69 return Err(format!("{name:?} is not a remote name"));
70 }
71 if choir_node::followers::remotes(&bare)
72 .iter()
73 .any(|(n, _)| n == name)
74 {
75 return Err(format!(
76 "{repo} already has a remote named {name}\n\n \
77 see it: git --git-dir {} remote -v",
78 bare.display()
79 ));
80 }
81 let out = std::process::Command::new("git")
82 .arg("--git-dir")
83 .arg(&bare)
84 .args(["remote", "add", name, url])
85 .output()
86 .map_err(|e| format!("could not run git: {e}"))?;
87 if !out.status.success() {
88 return Err(format!(
89 "git remote add refused: {}",
90 String::from_utf8_lossy(&out.stderr).trim()
91 ));
92 }
93 let marker = &layout.followers_marker;
94 if marker.exists() {
95 return Ok(false);
96 }
97 choir_fs::write_atomic(
98 marker,
99 "# Landings are pushed to every remote of their repository (D21).\n\
100 # Written by `choir repo follower add`; `choir node serve` passes\n\
101 # --followers while this file exists. Delete it to stop.\n",
102 )
103 .map_err(|e| format!("write {}: {e}", marker.display()))?;
104 Ok(true)
105}
106
107#[must_use]
109pub fn list(layout: &crate::serve::Layout) -> Vec<(String, Vec<(String, String)>)> {
110 repositories(layout)
111 .into_iter()
112 .map(|repo| {
113 let remotes = choir_node::followers::remotes(&layout.repos.join(&repo));
114 (repo, remotes)
115 })
116 .collect()
117}
118
119#[must_use]
121pub fn push(
122 layout: &crate::serve::Layout,
123 only: Option<&str>,
124) -> Vec<choir_node::followers::Outcome> {
125 let repos = match only {
126 Some(repo) => vec![repository_name(repo)],
127 None => repositories(layout),
128 };
129 repos
130 .iter()
131 .flat_map(|repo| choir_node::followers::push_all(&layout.repos, repo))
132 .collect()
133}
134
135#[must_use]
137pub fn following(layout: &crate::serve::Layout) -> bool {
138 layout.followers_marker.exists()
139}
140
141#[must_use]
143pub fn bare(layout: &crate::serve::Layout, repo: &str) -> std::path::PathBuf {
144 layout.repos.join(repository_name(repo))
145}