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}