grass/dev/strategy/git/
local.rs

1use git2::{build::RepoBuilder, ErrorCode, Repository, Status};
2use itertools::Itertools;
3use tracing::trace;
4
5use crate::dev::strategy::path::{PathStrategy, PathStrategyError};
6
7use super::{GitStrategy, GitStrategyError, RepositoryChangeStatus, RepositoryLocation, Result};
8
9pub struct LocalGitStrategy<'a, T: PathStrategy> {
10    path_strategy: &'a T,
11}
12
13impl<'a, T: PathStrategy> LocalGitStrategy<'a, T> {
14    pub fn new(path_strategy: &'a T) -> Self {
15        Self { path_strategy }
16    }
17}
18
19impl<'a, T: PathStrategy> GitStrategy for LocalGitStrategy<'a, T> {
20    fn clean<U>(&self, repository: U) -> Result<()>
21    where
22        U: Into<RepositoryLocation>,
23    {
24        let repository_path = self.path_strategy.get_directory(repository)?;
25
26        let repository = Repository::open(&repository_path)?;
27
28        let statuses = repository.statuses(None)?;
29
30        let results: Vec<_> = statuses
31            .iter()
32            .filter_map(|entry| match entry.path() {
33                Some(file_path) if entry.status().is_ignored() => Some(file_path.to_owned()),
34                _ => None,
35            })
36            .map(|file| {
37                let path = repository_path.join(file);
38                if path.is_dir() {
39                    std::fs::remove_dir_all(path)
40                } else {
41                    std::fs::remove_file(path)
42                }
43            })
44            .filter_map(std::result::Result::err)
45            .map(|error| error.to_string())
46            .collect();
47
48        match results.len() {
49            0 => Ok(()),
50            _ => Err(GitStrategyError::FileSystemError {
51                message: "Something went wrong when trying to clean a repository".into(),
52                reason: results.iter().map(ToString::to_string).join("\n"),
53                reasons: results,
54            }),
55        }
56    }
57
58    fn clone<U, V>(&self, repository: U, remote: V) -> Result<()>
59    where
60        U: Into<RepositoryLocation>,
61        V: AsRef<str>,
62    {
63        let repo_path = self.path_strategy.get_directory(repository)?;
64
65        RepoBuilder::new().clone(remote.as_ref(), &repo_path)?;
66
67        Ok(())
68    }
69
70    fn get_changes<U>(&self, repository: U) -> Result<RepositoryChangeStatus>
71    where
72        U: Into<RepositoryLocation>,
73    {
74        let repository_location = repository.into();
75        let repository_path = self
76            .path_strategy
77            .get_directory(repository_location.clone())?;
78
79        let repository = match Repository::open(repository_path) {
80            Ok(repository) => Ok(repository),
81            Err(error) => {
82                if error.code() == ErrorCode::NotFound {
83                    trace!(
84                        "Repository '{}' not found, assumed up to date",
85                        repository_location
86                    );
87                    return Ok(RepositoryChangeStatus::UpToDate);
88                }
89
90                Err(error)
91            }
92        };
93
94        let repository = repository.map_err(|error| {
95            GitStrategyError::from(error).with_message("There was a problem opening the repository")
96        })?;
97
98        let statuses = repository.statuses(None).map_err(|error| {
99            GitStrategyError::from(error)
100                .with_message("There was a problem retrieving the repository status")
101        })?;
102
103        let changes: Vec<_> = statuses
104            .iter()
105            .filter(|status| status.status().contains(Status::IGNORED))
106            .collect();
107
108        if changes.is_empty() {
109            trace!("Repository '{}' has no changes", repository_location);
110            return Ok(RepositoryChangeStatus::UpToDate);
111        }
112
113        trace!(
114            "Repository '{}' has ({}) changes",
115            repository_location,
116            changes.len()
117        );
118        Ok(RepositoryChangeStatus::UncommittedChanges {
119            num_changes: changes.len(),
120        })
121    }
122}
123
124impl From<PathStrategyError> for GitStrategyError {
125    fn from(value: PathStrategyError) -> Self {
126        match value {
127            PathStrategyError::RepositoryNotFound { context, reason } => {
128                GitStrategyError::RepositoryNotFound {
129                    message: context,
130                    reason,
131                }
132            }
133            PathStrategyError::FileDoesNotExist { context, reason } => {
134                GitStrategyError::FileSystemError {
135                    message: context,
136                    reason,
137                    reasons: Vec::new(),
138                }
139            }
140            PathStrategyError::Unknown { context, reason } => GitStrategyError::UnknownError {
141                message: context,
142                reason,
143            },
144        }
145    }
146}
147
148impl From<git2::Error> for GitStrategyError {
149    fn from(value: git2::Error) -> Self {
150        let reason = value.message().to_string();
151        match value.code() {
152            git2::ErrorCode::NotFound => GitStrategyError::RepositoryNotFound {
153                message: "There was a problem running an unspecified git2 command".into(),
154                reason,
155            },
156
157            git2::ErrorCode::Exists => GitStrategyError::RepositryExists {
158                message: "There was a problem running an unspecified git2 command".into(),
159                reason,
160            },
161
162            git2::ErrorCode::Ambiguous
163            | git2::ErrorCode::BufSize
164            | git2::ErrorCode::User
165            | git2::ErrorCode::BareRepo
166            | git2::ErrorCode::UnbornBranch
167            | git2::ErrorCode::Unmerged
168            | git2::ErrorCode::NotFastForward
169            | git2::ErrorCode::InvalidSpec
170            | git2::ErrorCode::Conflict
171            | git2::ErrorCode::Locked
172            | git2::ErrorCode::Modified
173            | git2::ErrorCode::Applied
174            | git2::ErrorCode::Peel
175            | git2::ErrorCode::Eof
176            | git2::ErrorCode::Invalid
177            | git2::ErrorCode::Uncommitted
178            | git2::ErrorCode::Directory
179            | git2::ErrorCode::MergeConflict
180            | git2::ErrorCode::HashsumMismatch
181            | git2::ErrorCode::IndexDirty
182            | git2::ErrorCode::ApplyFail
183            | git2::ErrorCode::Owner => GitStrategyError::RepositoryError {
184                message: "There was a problem running an unspecified git2 command".into(),
185                reason,
186            },
187
188            git2::ErrorCode::Auth | git2::ErrorCode::Certificate => {
189                GitStrategyError::RemoteAuthenticationError {
190                    message: "There was a problem running an unspecified git2 command".into(),
191                    reason,
192                }
193            }
194
195            _ => GitStrategyError::UnknownError {
196                message: "There was a problem running an unspecified git2 command".into(),
197                reason,
198            },
199        }
200    }
201}