workerlib/
workspace.rs

1//!
2//! Wrapper for running low level git commands and initializing the underlying
3//! mirror cache directory on a per-mirror basis
4//!
5
6use std::io::Error as IOError;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use git2::{Error as GitError, Repository, RepositoryInitOptions};
11
12/// Error encountered when initilizing a workspace
13#[derive(thiserror::Error, Debug)]
14pub enum Error {
15    /// Error while setting up the workspace on disk
16    #[error("Workspace Error (IO): {0}")]
17    IOError(#[from] IOError),
18
19    /// Libgit2 error
20    #[error("Workspace Error: Could not initialize repository: {0}")]
21    GitError(#[from] GitError),
22}
23
24/// Initialize the internal Lorry workspace reponsible for holding mirror and
25/// other relevant data. Workspace also contains a few lightweight methods that
26/// can return data about an existing mirror.
27///
28/// Directory structure:
29///
30/// git-repository  # bare repository that contains mirrored upstream code
31/// raw-files       # git worktree containing files synchronized via LFS
32///
33/// NOTE: Workspace methods are not asynchronous because these operations are
34/// all expected to take place on a single worker thread. Depending on the
35/// usage it may be desierable to wrap it in a tokio::Task. Since these are
36/// all IO operations and initialization steps blocking should be minimal.
37#[derive(Clone, Debug)]
38pub struct Workspace(pub PathBuf);
39
40impl Workspace {
41    /// Create a new Workspace based on the workdir and Lorry name
42    pub fn new(working_dir: &Path, name: &str) -> Self {
43        Workspace(working_dir.join(name.replace('/', "_")))
44    }
45
46    /// Return the path to the underlying git repository mirror
47    pub fn repository_path(&self) -> PathBuf {
48        self.0.join("git-repository")
49    }
50
51    /// Return the path to the underlying LFS data directory
52    pub fn lfs_data_path(&self) -> PathBuf {
53        self.0.join("raw-files")
54    }
55
56    /// Configure LFS in the underlying Git repo. NOTE: The repository must
57    /// already be initialized before running this.
58    fn enable_lfs(&self) -> Result<(), Error> {
59        // NOTE: using the std command interface here since this package is
60        // not async
61        Command::new("git")
62            .args(["lfs", "install", "--local"])
63            .current_dir(self.repository_path().as_path())
64            .output()?;
65        Ok(())
66    }
67
68    /// Determine if LFS is already setup on this repository by looking at its
69    /// git configuration file to see if was setup.
70    fn lfs_enabled(&self) -> Result<bool, Error> {
71        let repository = Repository::open(self.repository_path())?;
72        Ok(repository
73            .config()?
74            .get_entry("lfs.repositoryformatversion")
75            .is_ok())
76    }
77
78    /// Determine if the workspace appears to have a configured git repository
79    /// already setup within.
80    fn git_directory_is_initialized(&self, bare: bool) -> bool {
81        let test_paths = if bare {
82            [
83                self.0.join("git-repository/HEAD"),
84                self.0.join("git-repository/config"),
85                self.0.join("git-repository/objects"),
86            ]
87        } else {
88            [
89                self.0.join("git-repository/.git/HEAD"),
90                self.0.join("git-repository/.git/config"),
91                self.0.join("git-repository/.git/objects"),
92            ]
93        };
94        test_paths
95            .iter()
96            .all(|file_path| std::fs::metadata(file_path.as_path()).is_ok())
97    }
98
99    /// Return the HEAD of the repository within the workspace, NOTE: If the
100    /// workspace as just initialized then the HEAD will always return None.
101    pub fn head(&self) -> Option<String> {
102        Repository::open(self.0.join("git-repository"))
103            .and_then(|repository| {
104                repository
105                    .head()
106                    .map(|head| head.name().unwrap().to_string())
107            })
108            .ok()
109    }
110
111    /// Initialize the internal directories of the workspace if they are
112    /// missing otherwise this operation is NOOP. Returns true if the
113    /// repository was not already initialized and false if it was setup
114    /// already. If bare is specified the repository will be managed as "bare"
115    /// otherwise it will checkout repository contents of the main branch in
116    /// the repo path.
117    #[tracing::instrument(name = "workspace", skip_all)]
118    pub fn init_if_missing(&self, enable_lfs: bool, bare: bool) -> Result<bool, Error> {
119        [
120            self.0.clone(),
121            self.0.join("git-repository"),
122            self.0.join("raw-files"),
123        ]
124        .iter()
125        .try_fold((), |_, pb| {
126            tracing::debug!("ensuring directory: {:?} exists", self.0);
127            std::fs::create_dir_all(pb.as_path())
128        })?;
129        let mut initialized = false;
130        if !self.git_directory_is_initialized(bare) {
131            let repository_path = self.0.join("git-repository");
132            tracing::info!("Initializing new repository at {:?}", repository_path);
133            let repository = Repository::init_opts(
134                repository_path,
135                RepositoryInitOptions::new()
136                    .bare(bare)
137                    .initial_head(crate::git_config::DEFAULT_GIT_BRANCH),
138            )?;
139            // Update local config to indicate this repo is managed by Lorry
140            repository.config()?.set_bool("lorry.managed", true)?;
141            initialized = true;
142        }
143        if enable_lfs {
144            // Ensure LFS is setup
145            if !self.lfs_enabled()? {
146                self.enable_lfs()?;
147            }
148        };
149        tracing::info!("workspace initialized: {:?}", self.repository_path());
150        Ok(initialized)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use tempfile::tempdir;
158
159    #[test]
160    pub fn test_workspace_init() {
161        let workdir = tempdir().unwrap();
162        let workspace = Workspace(workdir.keep());
163        assert!(workspace.init_if_missing(false, true).unwrap());
164        // workspace is already setup so this returns false
165        assert!(!workspace.init_if_missing(false, true).unwrap());
166    }
167
168    #[test]
169    pub fn test_workspace_init_lfs() {
170        let workdir = tempdir().unwrap();
171        let workspace = Workspace(workdir.keep());
172        assert!(workspace.init_if_missing(true, true).unwrap());
173        // workspace is already setup so this returns false
174        assert!(!workspace.init_if_missing(true, true).unwrap());
175    }
176}