1use std::io::Error as IOError;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use git2::{Error as GitError, Repository, RepositoryInitOptions};
11
12#[derive(thiserror::Error, Debug)]
14pub enum Error {
15 #[error("Workspace Error (IO): {0}")]
17 IOError(#[from] IOError),
18
19 #[error("Workspace Error: Could not initialize repository: {0}")]
21 GitError(#[from] GitError),
22}
23
24#[derive(Clone, Debug)]
38pub struct Workspace(pub PathBuf);
39
40impl Workspace {
41 pub fn new(working_dir: &Path, name: &str) -> Self {
43 Workspace(working_dir.join(name.replace('/', "_")))
44 }
45
46 pub fn repository_path(&self) -> PathBuf {
48 self.0.join("git-repository")
49 }
50
51 pub fn lfs_data_path(&self) -> PathBuf {
53 self.0.join("raw-files")
54 }
55
56 fn enable_lfs(&self) -> Result<(), Error> {
59 Command::new("git")
62 .args(["lfs", "install", "--local"])
63 .current_dir(self.repository_path().as_path())
64 .output()?;
65 Ok(())
66 }
67
68 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 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 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 #[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 repository.config()?.set_bool("lorry.managed", true)?;
141 initialized = true;
142 }
143 if enable_lfs {
144 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 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 assert!(!workspace.init_if_missing(true, true).unwrap());
175 }
176}