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());
}
}