1use std::{
2 collections::{HashMap, HashSet},
3 io::{self, BufRead},
4 path::Path,
5 str::FromStr,
6};
7
8use anyhow::{anyhow, bail};
9
10use crate::{
11 data::{Branch, Link, Repo, View},
12 os,
13};
14
15impl Repo {
16 #[tracing::instrument]
17 pub async fn read_from_link(link: &Link) -> anyhow::Result<Self> {
18 let result = match link {
19 Link::Fs { dir } => Self::read_from_fs(dir).await,
20 Link::Net { url } => Self::read_from_url(url).await,
21 };
22 if let Err(error) = &result {
23 tracing::error!(?link, ?error, "Failed to read repo.");
24 }
25 result
26 }
27
28 #[tracing::instrument]
29 pub async fn read_from_fs<P>(dir: P) -> anyhow::Result<Self>
30 where
31 P: AsRef<Path> + std::fmt::Debug,
32 {
33 let dir = dir.as_ref();
34 let selph = Self {
35 description: description(dir).await?,
36 branches: branches(dir).await?,
37 remotes: remote_refs(dir).await?,
38 };
39 Ok(selph)
40 }
41
42 #[tracing::instrument]
43 pub async fn read_from_url(url: &str) -> anyhow::Result<Self> {
44 let dir = tempfile::tempdir()?;
45 let dir = dir.path();
46 clone_bare(url, dir).await?;
47 Self::read_from_fs(dir).await
48 }
49}
50
51#[derive(Debug)]
52struct TreeRef {
53 pub name: String,
54 pub hash: String,
55}
56
57impl FromStr for TreeRef {
58 type Err = anyhow::Error;
59
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 let mut fields = s.split_whitespace();
62 let hash = fields
63 .next()
64 .map(|str| str.to_string())
65 .ok_or_else(|| anyhow!("Ref line is empty: {s:?}"))?;
66 let name = fields
67 .next()
68 .map(|str| str.to_string())
69 .ok_or_else(|| anyhow!("Ref line missing path: {s:?}"))?;
70 if fields.next().is_some() {
71 bail!("Ref line has too many fields: {s:?}");
72 }
73 Ok(Self { name, hash })
74 }
75}
76
77#[derive(Debug)]
78struct RemoteRef {
79 pub name: String,
80 pub addr: String,
81}
82
83impl FromStr for RemoteRef {
84 type Err = anyhow::Error;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 let mut fields = s.split_whitespace();
88 let name = fields
89 .next()
90 .map(|str| str.to_string())
91 .ok_or_else(|| anyhow!("Remote line is empty: {s:?}"))?;
92 let addr = fields
93 .next()
94 .map(|str| str.to_string())
95 .ok_or_else(|| anyhow!("Remote line missing addr: {s:?}"))?;
96 Ok(Self { name, addr })
97 }
98}
99
100pub async fn view(host: &str, link: &Link) -> View {
101 View {
102 host: host.to_string(),
103 link: link.clone(),
104 repo: Repo::read_from_link(link).await.ok(),
105 }
106}
107
108pub async fn is_repo<P: AsRef<Path>>(dir: P) -> bool {
109 let git_dir = format!("--git-dir={}", dir.as_ref().to_string_lossy());
110 os::cmd("git", &[&git_dir, "log", "--format=", "-1"])
111 .await
112 .is_ok()
113}
114
115#[tracing::instrument(skip_all)]
116async fn branches(dir: &Path) -> anyhow::Result<HashMap<String, Branch>> {
117 let mut branches = HashMap::new();
118 for (name, leaf) in branch_leaves(dir).await? {
121 let roots = branch_roots(dir, &leaf).await?;
122 branches.insert(name, Branch { roots, leaf });
123 }
124 Ok(branches)
125}
126
127#[tracing::instrument(skip_all)]
128async fn branch_leaves(
129 dir: &Path,
130) -> anyhow::Result<HashMap<String, String>> {
131 let git_dir = format!("--git-dir={}", dir.to_string_lossy());
132 let mut refs = HashMap::new();
133 for line_result in os::cmd("git", &[&git_dir, "show-ref", "--branches"])
134 .await?
135 .lines()
136 {
137 let line: String = line_result?;
138 let TreeRef { name, hash } = line.parse()?;
139 if let Some(name) = name.strip_prefix("refs/heads/") {
140 refs.insert(name.to_string(), hash);
141 }
142 }
143 Ok(refs)
144}
145
146#[tracing::instrument(skip_all)]
147pub async fn clone_bare(
148 from_addr: &str,
149 to_dir: &Path,
150) -> anyhow::Result<()> {
151 let to_dir = to_dir.to_string_lossy().to_string();
152 let env = HashMap::from([
155 ("GIT_SSH_COMMAND", "ssh -oBatchMode=yes"),
156 ("GIT_TERMINAL_PROMPT", "0"),
157 ("GIT_ASKPASS", "echo"),
158 ("SSH_ASKPASS", "echo"),
159 ("GCM_INTERACTIVE", "never"),
160 ]);
161 let exe = "git";
162 let args = &["clone", "--bare", from_addr, &to_dir];
163 let out = tokio::process::Command::new(exe)
164 .args(args)
165 .envs(&env)
166 .output()
167 .await?;
168 out.status.success().then_some(()).ok_or_else(|| {
169 anyhow!(
170 "Failed to execute command: exe={exe:?} args={args:?} env={env:?} err={:?}",
171 String::from_utf8_lossy(&out.stderr[..])
172 )
173 })
174}
175
176#[tracing::instrument(skip_all)]
177async fn remote_refs(dir: &Path) -> anyhow::Result<HashMap<String, String>> {
178 let git_dir = format!("--git-dir={}", dir.to_string_lossy());
179 let mut remotes = HashMap::new();
180 for line_result in
181 os::cmd("git", &[&git_dir, "remote", "-v"]).await?.lines()
182 {
183 let line = line_result?;
184 let RemoteRef { name, addr } = line.parse()?;
185 remotes.insert(name, addr);
186 }
187 Ok(remotes)
188}
189
190#[tracing::instrument(skip(dir))]
191pub async fn branch_roots(
192 dir: &Path,
193 leaf_hash: &str,
194) -> anyhow::Result<HashSet<String>> {
195 let git_dir = format!("--git-dir={}", dir.to_string_lossy());
196 let output = os::cmd(
197 "git",
198 &[&git_dir, "rev-list", "--max-parents=0", leaf_hash, "--"],
199 )
200 .await?;
201 let roots: HashSet<String> =
202 output.lines().map_while(Result::ok).collect();
203 if roots.is_empty() {
204 bail!("Found 0 roots for leaf hash {leaf_hash} in repo={dir:?}");
205 }
206 Ok(roots)
207}
208
209#[tracing::instrument(skip_all)]
210pub async fn is_bare(dir: &Path) -> anyhow::Result<bool> {
211 let git_dir = format!("--git-dir={}", dir.to_string_lossy());
212 let out =
213 os::cmd("git", &[&git_dir, "rev-parse", "--is-bare-repository"])
214 .await?;
215 let out = String::from_utf8(out)?;
216 let is_bare: bool = out.trim().parse()?;
217 Ok(is_bare)
218}
219
220#[tracing::instrument(skip_all)]
221async fn description(dir: &Path) -> io::Result<Option<String>> {
222 tokio::fs::read_to_string(dir.join("description"))
223 .await
224 .map(|s| (!s.starts_with("Unnamed repository;")).then_some(s))
225}