1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//
use crate::{
    git::{
        repository::{
            open::{OpenRepository, OpenRepositoryLike},
            test::TestRepository,
        },
        validation::remotes::validate_default_remotes,
        RepoDetails,
    },
    GitDir, RemoteUrl,
};

use tracing::info;

pub mod factory;
pub mod open;
mod test;

#[allow(clippy::module_name_repetitions)]
pub use factory::RepositoryFactory;

#[cfg(test)]
mod tests;

#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Repository {
    Real,
    Test(TestRepository),
}

#[cfg(test)]
pub(crate) const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
    TestRepository::new(fs, vec![], vec![])
}

/// Opens a repository, cloning if necessary
#[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // requires network access to either clone new and/or fetch.
pub fn open(
    repository_factory: &dyn factory::RepositoryFactory,
    repo_details: &RepoDetails,
) -> Result<Box<dyn OpenRepositoryLike>> {
    let open_repository = if repo_details.gitdir.exists() {
        info!("Local copy found - opening...");
        repository_factory.open(repo_details)?
    } else {
        info!("Local copy not found - cloning...");
        repository_factory.git_clone(repo_details)?
    };
    info!("Validating...");
    validate_default_remotes(&*open_repository, repo_details)
        .map_err(|e| Error::Validation(e.to_string()))?;
    Ok(open_repository)
}

#[allow(clippy::module_name_repetitions)]
pub trait RepositoryLike {
    /// Opens the repository.
    ///
    /// # Errors
    ///
    /// Will return an `Err` if the repository can't be opened.
    fn open(&self, gitdir: &GitDir) -> Result<OpenRepository>;

    /// Clones the git repository from the remote server.
    ///
    /// # Errors
    ///
    /// Will return an `Err` if there are any network connectivity issues
    /// connecting with the server.
    fn git_clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository>;
}

#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum Direction {
    /// Push local changes to the remote.
    Push,
    /// Fetch changes from the remote to the local repository.
    Fetch,
}
impl From<Direction> for gix::remote::Direction {
    fn from(value: Direction) -> Self {
        match value {
            Direction::Push => Self::Push,
            Direction::Fetch => Self::Fetch,
        }
    }
}

pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("invalid git dir: {0}")]
    InvalidGitDir(GitDir),

    #[error("kxiofs: {0}")]
    KxioFs(#[from] kxio::fs::Error),

    #[error("io: {0}")]
    Io(std::io::Error),

    #[error("git exec wait: {0}")]
    Wait(std::io::Error),

    #[error("git exec spawn: {0}")]
    Spawn(std::io::Error),

    #[error("validation: {0}")]
    Validation(String),

    #[error("git clone: {0}")]
    Clone(String),

    #[error("open: {0}")]
    Open(String),

    #[error("git fetch: {0}")]
    Fetch(String),

    #[error("fake repository lock")]
    FakeLock,

    #[error("MismatchDefaultFetchRemote(found: {found:?}, expected: {expected:?})")]
    MismatchDefaultFetchRemote {
        found: Box<RemoteUrl>,
        expected: Box<RemoteUrl>,
    },
}

mod gix_errors {
    #![cfg(not(tarpaulin_include))] // third-party library errors
    use super::Error;
    impl From<gix::clone::Error> for Error {
        fn from(value: gix::clone::Error) -> Self {
            Self::Clone(value.to_string())
        }
    }
    impl From<gix::open::Error> for Error {
        fn from(value: gix::open::Error) -> Self {
            Self::Open(value.to_string())
        }
    }
    impl From<gix::clone::fetch::Error> for Error {
        fn from(value: gix::clone::fetch::Error) -> Self {
            Self::Fetch(value.to_string())
        }
    }
}