automaat_processor_git_clone/
lib.rs1#![deny(
50 clippy::all,
51 clippy::cargo,
52 clippy::nursery,
53 clippy::pedantic,
54 deprecated_in_future,
55 future_incompatible,
56 missing_docs,
57 nonstandard_style,
58 rust_2018_idioms,
59 rustdoc,
60 warnings,
61 unused_results,
62 unused_qualifications,
63 unused_lifetimes,
64 unused_import_braces,
65 unsafe_code,
66 unreachable_pub,
67 trivial_casts,
68 trivial_numeric_casts,
69 missing_debug_implementations,
70 missing_copy_implementations
71)]
72#![warn(variant_size_differences)]
73#![allow(clippy::multiple_crate_versions, missing_doc_code_examples)]
74#![doc(html_root_url = "https://docs.rs/automaat-processor-git-clone/0.1.0")]
75
76use automaat_core::{Context, Processor};
77use git2::{build::RepoBuilder, Cred, FetchOptions, RemoteCallbacks};
78use serde::{Deserialize, Serialize};
79use std::{error, fmt, path};
80use url::Url;
81
82#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
84#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
85pub struct GitClone {
86 #[serde(with = "url_serde")]
88 pub url: Url,
89
90 pub username: Option<String>,
92
93 pub password: Option<String>,
95
96 pub path: Option<String>,
100}
101
102#[cfg(feature = "juniper")]
109#[cfg_attr(feature = "juniper", derive(juniper::GraphQLInputObject))]
110#[cfg_attr(feature = "juniper", graphql(name = "GitCloneInput"))]
111#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
112pub struct Input {
113 #[serde(with = "url_serde")]
114 url: Url,
115
116 username: Option<String>,
117 password: Option<String>,
118 path: Option<String>,
119}
120
121#[cfg(feature = "juniper")]
122impl From<Input> for GitClone {
123 fn from(input: Input) -> Self {
124 Self {
125 username: input.username,
126 password: input.password,
127 url: input.url,
128 path: input.path,
129 }
130 }
131}
132
133impl<'a> Processor<'a> for GitClone {
134 const NAME: &'static str = "Git Clone";
135
136 type Error = Error;
137 type Output = String;
138
139 fn validate(&self) -> Result<(), Self::Error> {
151 if let Some(path) = &self.path {
152 let path = path::Path::new(path);
153
154 path.components().try_for_each(|c| match c {
155 path::Component::Normal(_) => Ok(()),
156 _ => Err(Error::Path),
157 })?;
158 };
159
160 Ok(())
161 }
162
163 fn run(&self, context: &Context) -> Result<Option<Self::Output>, Self::Error> {
176 let mut callbacks = RemoteCallbacks::new();
177 let mut fetch_options = FetchOptions::new();
178 let workspace = context.workspace_path();
179 let path = self
180 .path
181 .as_ref()
182 .map_or_else(|| workspace.into(), |path| workspace.join(path));
183
184 if let (Some(u), Some(p)) = (&self.username, &self.password) {
185 let _ = callbacks.credentials(move |_, _, _| Cred::userpass_plaintext(u, p));
186 let _ = fetch_options.remote_callbacks(callbacks);
187 };
188
189 RepoBuilder::new()
190 .fetch_options(fetch_options)
191 .clone(self.url.as_str(), &path)
192 .map(|_| None)
193 .map_err(Into::into)
194 }
195}
196
197#[derive(Debug)]
202pub enum Error {
203 Path,
205
206 Git(git2::Error),
208
209 #[doc(hidden)]
210 __Unknown, }
212
213impl fmt::Display for Error {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 match *self {
216 Error::Path => write!(f, "Path error: invalid path location"),
217 Error::Git(ref err) => write!(f, "Git error: {}", err),
218 Error::__Unknown => unreachable!(),
219 }
220 }
221}
222
223impl error::Error for Error {
224 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
225 match *self {
226 Error::Path => None,
227 Error::Git(ref err) => Some(err),
228 Error::__Unknown => unreachable!(),
229 }
230 }
231}
232
233impl From<git2::Error> for Error {
234 fn from(err: git2::Error) -> Self {
235 Error::Git(err)
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn processor_stub() -> GitClone {
244 GitClone {
245 username: None,
246 password: None,
247 url: Url::parse("http://127.0.0.1").unwrap(),
248 path: None,
249 }
250 }
251
252 mod validate {
253 use super::*;
254
255 #[test]
256 fn no_path() {
257 let mut processor = processor_stub();
258 processor.path = None;
259
260 processor.validate().unwrap()
261 }
262
263 #[test]
264 fn relative_path() {
265 let mut processor = processor_stub();
266 processor.path = Some("hello/world".to_owned());
267
268 processor.validate().unwrap()
269 }
270
271 #[test]
272 #[should_panic]
273 fn prefix_path() {
274 let mut processor = processor_stub();
275 processor.path = Some("../parent".to_owned());
276
277 processor.validate().unwrap()
278 }
279
280 #[test]
281 #[should_panic]
282 fn absolute_path() {
283 let mut processor = processor_stub();
284 processor.path = Some("/etc".to_owned());
285
286 processor.validate().unwrap()
287 }
288 }
289
290 #[test]
291 fn test_readme_deps() {
292 version_sync::assert_markdown_deps_updated!("README.md");
293 }
294
295 #[test]
296 fn test_html_root_url() {
297 version_sync::assert_html_root_url_updated!("src/lib.rs");
298 }
299}