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
152
153
154
155
156
157
158
159
160
use std::io::Error as IOError;
use std::path::{Path, PathBuf};
use std::process::Command;

use git2::{Error as GitError, Repository, RepositoryInitOptions};

/// Error encountered when initilizing a workspace
#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Workspace Error (IO): {0}")]
    IOError(#[from] IOError),
    #[error("Workspace Error: Could not initialize repository: {0}")]
    GitError(#[from] GitError),
}

/// Initialize the internal Lorry workspace reponsible for holding mirror and
/// other relavent data. Workspace also contains a few lightweight methods that
/// can return data about an existing mirror.
///
/// Directory structure:
///
/// git-repository  # bare repository that contains mirrored upstream code
/// raw-files       # git worktree containing files synchronized via LFS
///
/// NOTE: Workspace methods are not asynchronous because these operations are
/// all expected to take place on a single worker thread. Depending on the
/// usage it may be desierable to wrap it in a tokio::Task. Since these are
/// all IO operations and initialization steps blocking should be minimal.
#[derive(Clone, Debug)]
pub struct Workspace(pub PathBuf);

impl Workspace {
    /// Create a new Workspace based on the workdir and Lorry name
    pub fn new(working_dir: &Path, name: &str) -> Self {
        Workspace(working_dir.join(name.replace('/', "_")))
    }

    /// Return the path to the underlying git repository mirror
    pub fn repository_path(&self) -> PathBuf {
        self.0.join("git-repository")
    }

    /// Return the path to the underlying LFS data directory
    pub fn lfs_data_path(&self) -> PathBuf {
        self.0.join("raw-files")
    }

    /// Configure LFS in the underlying Git repo. NOTE: The repository must
    /// already be initialized before running this.
    fn enable_lfs(&self) -> Result<(), Error> {
        // NOTE: using the std command interface here since this package is
        // not async
        Command::new("git")
            .args(["lfs", "install", "--local"])
            .current_dir(self.repository_path().as_path())
            .output()?;
        Ok(())
    }

    /// Determine if LFS is already setup on this repository by looking at its
    /// git configuration file to see if was setup.
    fn lfs_enabled(&self) -> Result<bool, Error> {
        let repository = Repository::open(self.repository_path())?;
        Ok(repository
            .config()?
            .get_entry("lfs.repositoryformatversion")
            .is_ok())
    }

    /// Determine if the workspace appears to have a configured git repository
    /// already setup within.
    fn git_directory_is_initialized(&self) -> bool {
        [
            self.0.join("git-repository/HEAD"),
            self.0.join("git-repository/config"),
            self.0.join("git-repository/objects"),
        ]
        .iter()
        .fold(true, |is_git_repo, pb| {
            if is_git_repo {
                std::fs::metadata(pb.as_path()).is_ok()
            } else {
                false
            }
        })
    }

    /// return the HEAD of the repository within the workspace, NOTE: If the
    /// workspace as just initialized then the HEAD will always return None.
    pub fn head(&self) -> Option<String> {
        Repository::open(self.0.join("git-repository"))
            .and_then(|repository| {
                repository
                    .head()
                    .map(|head| head.name().unwrap().to_string())
            })
            .ok()
    }

    /// Initialize the internal directories of the workspace if they are
    /// missing otherwise this operation is NOOP. Returns true if the
    /// repository was not already initialized and false if it was setup
    /// already.
    pub fn init_if_missing(&self, enable_lfs: bool) -> Result<bool, Error> {
        [
            self.0.clone(),
            self.0.join("git-repository"),
            self.0.join("raw-files"),
        ]
        .iter()
        .try_fold((), |_, pb| {
            tracing::debug!("ensuring directory: {:?} exists", self.0);
            std::fs::create_dir_all(pb.as_path())
        })?;
        let mut initialized = false;
        if !self.git_directory_is_initialized() {
            let repository_path = self.0.join("git-repository");
            tracing::info!("Initializing new repository at {:?}", repository_path);
            let repository = Repository::init_opts(
                repository_path,
                RepositoryInitOptions::new()
                    .bare(true)
                    .initial_head(crate::git_config::DEFAULT_GIT_BRANCH),
            )?;
            // update local config to indicate this repo is managed by Lorry
            repository.config()?.set_bool("lorry.managed", true)?;
            initialized = true;
        }
        if enable_lfs {
            // ensure LFS is setup
            if !self.lfs_enabled()? {
                self.enable_lfs()?;
            }
        };
        Ok(initialized)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;
    #[test]
    pub fn test_workspace_init() {
        let workdir = tempdir().unwrap();
        let workspace = Workspace(workdir.into_path());
        assert!(workspace.init_if_missing(false).unwrap());
        // workspace is already setup so this returns false
        assert!(!workspace.init_if_missing(false).unwrap());
    }

    #[test]
    pub fn test_workspace_init_lfs() {
        let workdir = tempdir().unwrap();
        let workspace = Workspace(workdir.into_path());
        assert!(workspace.init_if_missing(true).unwrap());
        // workspace is already setup so this returns false
        assert!(!workspace.init_if_missing(true).unwrap());
    }
}