grass/dev/strategy/git.rs
1mod local;
2mod mock;
3use std::fmt::Display;
4
5use thiserror::Error;
6
7pub use local::LocalGitStrategy;
8pub use mock::MockGitStrategy;
9
10use crate::{
11 dev::{error::GrassError, public::api::RepositoryLocation},
12 support_strategy,
13};
14
15use super::alias::AliasStrategyError;
16
17/// Error returned from methods in `GitStrategy`[^strategy].
18///
19/// # Todo:
20///
21/// - [ ] Change to `context` and `reason` fields.
22///
23/// [^strategy]: [crate::dev::strategy::git::GitStrategy]
24#[derive(Error, Debug, PartialEq, Eq, Hash)]
25pub enum GitStrategyError {
26 #[error("Cannot find repository:\n{message}\nReason: {reason}")]
27 RepositoryNotFound { message: String, reason: String },
28 #[error("There is a problem with the repository:\n{message}\nReason: {reason}")]
29 RepositoryError { message: String, reason: String },
30 #[error("The repository already exists:\n{message}\nReason: {reason}")]
31 RepositryExists { message: String, reason: String },
32 #[error("There is a problem fetching a remote:\n{message}\nReason: {reason}")]
33 RemoteFetchError { message: String, reason: String },
34 #[error("There is a problem authenticating for a remote:\n{message}\nReason: {reason}")]
35 RemoteAuthenticationError { message: String, reason: String },
36 #[error("There is a problem accessing the file system:\n{message}\nReason: {reason}")]
37 FileSystemError {
38 message: String,
39 reason: String,
40 reasons: Vec<String>,
41 },
42 #[error("There is a problem:\n{message}\nReason: {reason}")]
43 UnknownError { message: String, reason: String },
44}
45
46/// Alias for results in methods from `GitStrategy`[^strategy]
47///
48/// [^strategy]: [crate::dev::strategy::git::GitStrategy]
49pub type Result<T> = std::result::Result<T, GitStrategyError>;
50
51/// Describes the status of a repository.
52///
53/// The status is related to whether or not there are changes.
54#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
55pub enum RepositoryChangeStatus {
56 /// All changes have been committed.
57 UpToDate,
58 /// No repository has been initialized.
59 NoRepository,
60 /// A repository has been initialized, but there are uncommitted changes.
61 ///
62 /// `num_changes` is not strongly defined, this number may change between versions.
63 /// It has no real meaning, and should only be used for generic estimates.
64 UncommittedChanges { num_changes: usize },
65 /// This repository has an unknown status.
66 ///
67 /// This is only applicable if the unkown status is within the expected behavior of the
68 /// implementation.
69 /// For example, if an implementation has to synchronize in the background, then it may result
70 /// in an unkown status.
71 ///
72 /// This means that unknown doesn't mean something is wrong, although it may be.
73 /// If the status is unknown due to an error, use `Error`[^error] instead.
74 ///
75 /// [^error]: [crate::dev::strategy::git::RepositoryChangeStatus::Error]
76 #[default]
77 Unknown,
78}
79
80impl Display for RepositoryChangeStatus {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 RepositoryChangeStatus::UpToDate => write!(f, "Up to date"),
84 RepositoryChangeStatus::NoRepository => write!(f, "Missing repository"),
85 RepositoryChangeStatus::UncommittedChanges { num_changes } => {
86 write!(f, "({}) Uncommitted changes", num_changes)
87 }
88 RepositoryChangeStatus::Unknown => write!(f, "Status unknown"),
89 }
90 }
91}
92
93/// Describes the status of a repository.
94///
95/// The status is related to whether or not there are changes.
96#[derive(Debug, PartialEq, Eq, Hash)]
97pub enum RepositoryChangeStatusWithError {
98 /// All changes have been committed.
99 UpToDate,
100 /// No repository has been initialized.
101 NoRepository,
102 /// A repository has been initialized, but there are uncommitted changes.
103 ///
104 /// `num_changes` is not strongly defined, this number may change between versions.
105 /// It has no real meaning, and should only be used for generic estimates.
106 UncommittedChanges { num_changes: usize },
107 /// The status is unkown due to an error.
108 Error { reason: GrassError },
109 /// This repository has an unknown status.
110 ///
111 /// This is only applicable if the unkown status is within the expected behavior of the
112 /// implementation.
113 /// For example, if an implementation has to synchronize in the background, then it may result
114 /// in an unkown status.
115 ///
116 /// This means that unknown doesn't mean something is wrong, although it may be.
117 /// If the status is unknown due to an error, use `Error`[^error] instead.
118 ///
119 /// [^error]: [crate::dev::strategy::git::RepositoryChangeStatus::Error]
120 Unknown,
121}
122
123/// Strategy for all git operations.
124///
125/// Apply git operations on a repository.
126/// Operations can return information, or mutate the repository.
127///
128/// # Implementations
129///
130/// | Strategy | Description |
131/// | :-------------------------------------------- | :---------------------------- |
132/// | [crate::dev::strategy::git::LocalGitStrategy] | Access local git repositories |
133/// | [crate::dev::strategy::git::MockGitStrategy] | Mocking implementation |
134///
135/// # See
136///
137/// - [crate::dev::strategy::git::SupportsGit]
138pub trait GitStrategy {
139 /// Clean the repository from files that are explicitely ignored.
140 ///
141 /// This will **not** delete uncommitted changes.
142 /// Rather it will clean files which exists, but are ignored.
143 /// It's purpose then is to clean out optional files,
144 /// for example to optimize disk space.
145 ///
146 /// # Example
147 ///
148 /// ```rust
149 /// use grass::dev::strategy::git::{GitStrategy, GitStrategyError, MockGitStrategy};
150 /// let strategy = MockGitStrategy;
151 ///
152 /// fn test_strategy<T: GitStrategy>(strategy: &T) {
153 /// assert_eq!(strategy.clean(("all_good", "first")), Ok(()));
154 ///
155 /// assert!(matches!(
156 /// strategy.clean(("with_error", "first")),
157 /// Err(GitStrategyError::RepositoryError { .. })
158 /// ));
159 ///
160 /// assert!(matches!(
161 /// strategy.clean(("with_error", "second")),
162 /// Err(GitStrategyError::FileSystemError { .. })
163 /// ));
164 ///
165 /// assert!(matches!(
166 /// strategy.clean(("missing", "first")),
167 /// Err(GitStrategyError::RepositoryNotFound { .. })
168 /// ));
169 /// }
170 ///
171 /// test_strategy(&strategy);
172 /// ```
173 fn clean<T>(&self, repository: T) -> Result<()>
174 where
175 T: Into<RepositoryLocation>;
176
177 /// Clone a remote repository.
178 ///
179 /// # Todo:
180 ///
181 /// - [ ] Support authentication:
182 /// - [ ] SSH
183 /// - [ ] Password
184 /// - [ ] PAT
185 ///
186 /// # Example
187 ///
188 /// ```rust
189 /// use grass::dev::strategy::git::{GitStrategy, GitStrategyError, MockGitStrategy};
190 /// let strategy = MockGitStrategy;
191 ///
192 /// fn test_strategy<T: GitStrategy>(strategy: &T) {
193 /// assert_eq!(strategy.clone(("all_good", "new"), "good_remote"), Ok(()));
194 ///
195 /// assert!(matches!(
196 /// strategy.clone(("all_good", "first"), "good_remote"),
197 /// Err(GitStrategyError::RepositryExists { .. })
198 /// ));
199 ///
200 /// assert!(matches!(
201 /// strategy.clone(("missing", "first"), "good_remote"),
202 /// Err(GitStrategyError::RepositoryNotFound { .. })
203 /// ));
204 ///
205 /// assert!(matches!(
206 /// strategy.clone(("all_good", "new"), "no_access"),
207 /// Err(GitStrategyError::RemoteAuthenticationError { .. })
208 /// ));
209 ///
210 /// assert!(matches!(
211 /// strategy.clone(("all_good", "new"), "bad_response"),
212 /// Err(GitStrategyError::RemoteFetchError { .. })
213 /// ));
214 /// }
215 ///
216 /// test_strategy(&strategy);
217 /// ```
218 fn clone<T, U>(&self, repository: T, remote: U) -> Result<()>
219 where
220 T: Into<RepositoryLocation>,
221 U: AsRef<str>;
222
223 /// Get the change status for a repository.
224 ///
225 /// # Example
226 ///
227 /// ```rust
228 /// use grass::dev::strategy::git::{GitStrategy, MockGitStrategy, RepositoryChangeStatus};
229 /// let strategy = MockGitStrategy;
230 ///
231 /// fn test_strategy<T: GitStrategy>(strategy: &T) {
232 /// assert_eq!(
233 /// strategy.get_changes(("with_changes", "first")),
234 /// Ok(RepositoryChangeStatus::UpToDate)
235 /// );
236 ///
237 /// assert_eq!(
238 /// strategy.get_changes(("with_changes", "second")),
239 /// Ok(RepositoryChangeStatus::NoRepository)
240 /// );
241 ///
242 /// assert_eq!(
243 /// strategy.get_changes(("with_changes", "third")),
244 /// Ok(RepositoryChangeStatus::UncommittedChanges { num_changes: 9 })
245 /// );
246 /// }
247 ///
248 /// test_strategy(&strategy);
249 /// ```
250 fn get_changes<T>(&self, repository: T) -> Result<RepositoryChangeStatus>
251 where
252 T: Into<RepositoryLocation>;
253}
254
255support_strategy!(SupportsGit, get_git_strategy, GitStrategy);
256
257impl GitStrategyError {
258 pub fn with_message<T>(self, message: T) -> Self
259 where
260 T: Into<String>,
261 {
262 use GitStrategyError::*;
263
264 let message = message.into();
265 match self {
266 RepositoryNotFound { reason, .. } => RepositoryNotFound { message, reason },
267 RepositoryError { reason, .. } => RepositoryError { message, reason },
268 RepositryExists { reason, .. } => RepositryExists { message, reason },
269 RemoteFetchError { reason, .. } => RemoteFetchError { message, reason },
270 RemoteAuthenticationError { reason, .. } => {
271 RemoteAuthenticationError { message, reason }
272 }
273 FileSystemError {
274 reason, reasons, ..
275 } => FileSystemError {
276 message,
277 reason,
278 reasons,
279 },
280 UnknownError { reason, .. } => UnknownError { message, reason },
281 }
282 }
283}
284
285impl From<AliasStrategyError> for GitStrategyError {
286 fn from(value: AliasStrategyError) -> Self {
287 match value {
288 AliasStrategyError::UnkownError { context, reason } => GitStrategyError::UnknownError {
289 message: context,
290 reason,
291 },
292 AliasStrategyError::CategoryNotFound { context, reason } => {
293 GitStrategyError::RepositoryNotFound {
294 message: context,
295 reason,
296 }
297 }
298 }
299 }
300}
301
302impl<T: Into<GrassError>> From<std::result::Result<RepositoryChangeStatus, T>>
303 for RepositoryChangeStatusWithError
304{
305 fn from(value: std::result::Result<RepositoryChangeStatus, T>) -> Self {
306 match value {
307 Ok(change_status) => change_status.into(),
308 Err(error) => Into::<RepositoryChangeStatusWithError>::into(error),
309 }
310 }
311}
312
313impl From<RepositoryChangeStatus> for RepositoryChangeStatusWithError {
314 fn from(value: RepositoryChangeStatus) -> Self {
315 match value {
316 RepositoryChangeStatus::UpToDate => RepositoryChangeStatusWithError::UpToDate,
317 RepositoryChangeStatus::NoRepository => RepositoryChangeStatusWithError::NoRepository,
318 RepositoryChangeStatus::UncommittedChanges { num_changes } => {
319 RepositoryChangeStatusWithError::UncommittedChanges { num_changes }
320 }
321 RepositoryChangeStatus::Unknown => RepositoryChangeStatusWithError::Unknown,
322 }
323 }
324}
325
326impl<T: Into<GrassError>> From<T> for RepositoryChangeStatusWithError {
327 fn from(value: T) -> Self {
328 RepositoryChangeStatusWithError::Error {
329 reason: value.into(),
330 }
331 }
332}