1use std::{
2 fs::Permissions,
3 io::Write,
4 os::unix::fs::PermissionsExt,
5 path::{Path, PathBuf},
6 str::FromStr,
7};
8
9use tempfile::NamedTempFile;
10use time::{
11 format_description::{well_known::Rfc2822, FormatItem},
12 macros::format_description,
13 parsing::Parsable,
14 OffsetDateTime, PrimitiveDateTime,
15};
16
17use radicle::{
18 cob::ObjectId,
19 git::Oid,
20 prelude::{NodeId, RepoId},
21 storage::ReadStorage,
22 Profile, Storage,
23};
24
25pub fn lookup_repo(profile: &Profile, wanted: &str) -> Result<(RepoId, String), UtilError> {
26 let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::Storage)?;
27
28 let repos = storage.repositories().map_err(UtilError::Repositories)?;
29 let mut rid = None;
30
31 if let Ok(wanted_rid) = RepoId::from_urn(wanted) {
32 for ri in repos {
33 let project = ri
34 .doc
35 .project()
36 .map_err(|e| UtilError::Project(ri.rid, e))?;
37
38 if ri.rid == wanted_rid {
39 if rid.is_some() {
40 return Err(UtilError::DuplicateRepositories(wanted.into()));
41 }
42 rid = Some((ri.rid, project.name().to_string()));
43 }
44 }
45 } else {
46 for ri in repos {
47 let project = ri
48 .doc
49 .project()
50 .map_err(|e| UtilError::Project(ri.rid, e))?;
51
52 if project.name() == wanted {
53 if rid.is_some() {
54 return Err(UtilError::DuplicateRepositories(wanted.into()));
55 }
56 rid = Some((ri.rid, project.name().to_string()));
57 }
58 }
59 }
60
61 if let Some(rid) = rid {
62 Ok(rid)
63 } else {
64 Err(UtilError::NotFound(wanted.into()))
65 }
66}
67
68pub fn oid_from_cli_arg(profile: &Profile, rid: RepoId, commit: &str) -> Result<Oid, UtilError> {
69 if let Ok(oid) = Oid::from_str(commit) {
70 Ok(oid)
71 } else {
72 lookup_commit(profile, rid, commit)
73 }
74}
75
76pub fn load_profile() -> Result<Profile, UtilError> {
77 Profile::load().map_err(UtilError::Profile)
78}
79
80pub fn lookup_nid(profile: &Profile) -> Result<NodeId, UtilError> {
81 Ok(*profile.id())
82}
83
84pub fn lookup_commit(profile: &Profile, rid: RepoId, gitref: &str) -> Result<Oid, UtilError> {
85 let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::Storage)?;
86 let repo = storage
87 .repository(rid)
88 .map_err(|e| UtilError::RepoOpen(rid, e))?;
89 let object = repo
90 .backend
91 .revparse_single(gitref)
92 .map_err(|e| UtilError::RevParse(gitref.into(), e))?;
93
94 Ok(object.id().into())
95}
96
97pub fn now() -> Result<String, UtilError> {
98 let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
99 OffsetDateTime::now_utc()
100 .format(fmt)
101 .map_err(UtilError::TimeFormat)
102}
103
104pub fn parse_timestamp(timestamp: &str) -> Result<OffsetDateTime, UtilError> {
105 const SIMPLIFIED_ISO8601_WITH_Z: &[FormatItem<'static>] =
106 format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
107
108 fn parse_one(
109 timestamp: &str,
110 fmt: &(impl Parsable + ?Sized),
111 ) -> Result<OffsetDateTime, time::error::Parse> {
112 let r = PrimitiveDateTime::parse(timestamp, fmt);
113 if let Ok(t) = r {
114 Ok(t.assume_utc())
115 } else {
116 #[allow(clippy::unwrap_used)]
117 Err(r.err().unwrap())
118 }
119 }
120
121 if let Ok(t) = parse_one(timestamp, SIMPLIFIED_ISO8601_WITH_Z) {
122 Ok(t)
123 } else {
124 Err(UtilError::TimestampParse(timestamp.into()))
125 }
126}
127
128pub fn rfc822_timestamp(ts: &OffsetDateTime) -> Result<String, UtilError> {
129 let ts = ts.format(&Rfc2822).map_err(UtilError::TimeFormat)?;
130 Ok(ts.to_string())
131}
132
133pub fn read_file_as_string(filename: &Path) -> Result<String, UtilError> {
134 String::from_utf8(
135 std::fs::read(filename).map_err(|err| UtilError::Readfile(filename.into(), err))?,
136 )
137 .map_err(|err| UtilError::Utf8(filename.into(), err))
138}
139
140pub fn read_file_as_objectid(filename: &Path) -> Result<ObjectId, UtilError> {
141 let s = read_file_as_string(filename)?;
142 ObjectId::from_str(s.trim()).map_err(|err| UtilError::ReadObjectId(filename.into(), err))
143}
144
145pub fn safely_overwrite<P: AsRef<Path>>(filename: P, data: &[u8]) -> Result<(), UtilError> {
146 let filename = filename.as_ref();
147 let dirname = filename
148 .parent()
149 .ok_or(UtilError::NoParent(filename.to_path_buf()))?;
150 let mut tmp = NamedTempFile::new_in(dirname)
151 .map_err(|err| UtilError::CreateTemp(dirname.to_path_buf(), err))?;
152 tmp.write_all(data)
153 .map_err(|err| UtilError::WriteTemp(dirname.to_path_buf(), err))?;
154 let mode = Permissions::from_mode(0o644);
155 std::fs::set_permissions(tmp.path(), mode).map_err(UtilError::TempPerm)?;
156 tmp.persist(filename)
157 .map_err(|err| UtilError::RenameTemp(filename.to_path_buf(), err))?;
158 Ok(())
159}
160
161#[derive(Debug, thiserror::Error)]
162pub enum UtilError {
163 #[error("failed to look up node profile")]
164 Profile(#[source] radicle::profile::Error),
165
166 #[error("failed to look up open node storage")]
167 Storage(#[source] radicle::storage::Error),
168
169 #[error("failed to list repositories in node storage")]
170 Repositories(#[source] radicle::storage::Error),
171
172 #[error("failed to look up project info for repository {0}")]
173 Project(RepoId, #[source] radicle::identity::doc::PayloadError),
174
175 #[error("node has more than one repository called {0}")]
176 DuplicateRepositories(String),
177
178 #[error("node has no repository called: {0}")]
179 NotFound(String),
180
181 #[error("failed to open git repository in node storage: {0}")]
182 RepoOpen(RepoId, #[source] radicle::storage::RepositoryError),
183
184 #[error("failed to parse git ref as a commit id: {0}")]
185 RevParse(String, #[source] radicle::git::raw::Error),
186
187 #[error("failed to format time stamp")]
188 TimeFormat(#[source] time::error::Format),
189
190 #[error("failed to parse timestamp {0:?}")]
191 TimestampParse(String),
192
193 #[error("failed to read file {0}")]
194 Readfile(PathBuf, #[source] std::io::Error),
195
196 #[error("failed to convert file to UTF8: {0}")]
197 Utf8(PathBuf, #[source] std::string::FromUtf8Error),
198
199 #[error("failed to read object id from {0}")]
200 ReadObjectId(PathBuf, #[source] radicle::cob::object::ParseObjectId),
201
202 #[error("file name to write to doesn't have a parent directory: {0}")]
203 NoParent(PathBuf),
204
205 #[error("failed to create temporary file in directory {0}")]
206 CreateTemp(PathBuf, #[source] std::io::Error),
207
208 #[error("failed to write to temporary file in {0}")]
209 WriteTemp(PathBuf, #[source] std::io::Error),
210
211 #[error("failed to set permissions on temporary file")]
212 TempPerm(#[source] std::io::Error),
213
214 #[error("failed to rename temporary file to {0}")]
215 RenameTemp(PathBuf, #[source] tempfile::PersistError),
216}