radicle_ci_broker/
util.rs

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}