diff options
| author | Max Audron <audron@cocaine.farm> | 2022-06-07 12:28:18 +0200 |
|---|---|---|
| committer | Maximilian Manz <maximilian.manz@de.clara.net> | 2022-06-20 11:33:04 +0200 |
| commit | e9dc01ffb547d0fa605bfe38b34672ddd5161be4 (patch) | |
| tree | 5ca50547512b7cc2256ef457d468c4252ae23a0b /src/repo | |
| parent | implement cloning of new repos (diff) | |
reorganize file structure and cleanup lints
Diffstat (limited to 'src/repo')
| -rw-r--r-- | src/repo/aggregate.rs | 105 | ||||
| -rw-r--r-- | src/repo/mod.rs | 229 | ||||
| -rw-r--r-- | src/repo/repostate.rs | 36 |
3 files changed, 370 insertions, 0 deletions
diff --git a/src/repo/aggregate.rs b/src/repo/aggregate.rs new file mode 100644 index 0000000..cb4b00d --- /dev/null +++ b/src/repo/aggregate.rs @@ -0,0 +1,105 @@ +use git2::Repository; + +use tracing::error; +use walkdir::WalkDir; + +use crate::forge::Project; + +use super::{Repo, Repos}; + +#[async_trait::async_trait] +pub trait Aggregator { + async fn from_local(root: &str, scope: &str) -> Repos; + async fn from_forge(root: &str, projects: Vec<Project>) -> Repos; + async fn aggregate(mut local: Repos, mut remote: Repos) -> Repos; +} + +#[async_trait::async_trait] +impl Aggregator for Repos { + #[tracing::instrument(level = "trace")] + async fn from_local(root: &str, scope: &str) -> Repos { + let mut repos = Vec::new(); + + let path: std::path::PathBuf = [root, scope].iter().collect(); + + if !path.exists() { + return repos; + } + + let mut walker = WalkDir::new(path).into_iter(); + + loop { + let entry = match walker.next() { + None => break, + Some(Err(err)) => panic!("ERROR: {}", err), + Some(Ok(entry)) => entry, + }; + + if entry.file_type().is_dir() { + let mut dir = std::fs::read_dir(entry.path()).unwrap(); + + if dir.any(|dir| { + if let Ok(dir) = dir { + dir.file_name() == ".git" + } else { + false + } + }) { + walker.skip_current_dir(); + + match Repository::open(entry.path()) { + Ok(repo) => repos.push(Repo { + name: entry + .path() + .strip_prefix(root) + .unwrap() + .to_str() + .unwrap() + .to_string(), + path: entry.path().to_path_buf(), + repo: Some(repo), + ..Repo::default() + }), + Err(err) => error!("could not open repository: {}", err), + } + } else { + continue; + } + } + } + + return repos; + } + + #[tracing::instrument(level = "trace")] + async fn from_forge(root: &str, projects: Vec<Project>) -> Repos { + projects + .iter() + .map(|project| { + let mut repo: Repo = project.into(); + repo.path = [root, &repo.name].iter().collect(); + repo + }) + .collect() + } + + #[tracing::instrument(level = "trace", skip(local, remote))] + async fn aggregate(mut local: Repos, mut remote: Repos) -> Repos { + local = local + .into_iter() + .map(|mut left| { + if let Some(i) = remote.iter().position(|right| *right == left) { + let right = remote.remove(i); + left.forge = right.forge; + } + + left + }) + .collect(); + + local.append(&mut remote); + local.sort(); + + return local; + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs new file mode 100644 index 0000000..e3d1279 --- /dev/null +++ b/src/repo/mod.rs @@ -0,0 +1,229 @@ +use std::{fmt::Debug, path::PathBuf}; + +use thiserror::Error; + +use git2::{Branch, Remote, Repository}; +use tracing::{debug, trace}; + +use crate::forge::Project; + +mod aggregate; +mod repostate; + +pub use aggregate::*; +pub use repostate::*; + +pub type Repos = Vec<Repo>; + +pub struct Repo { + pub name: String, + pub path: PathBuf, + pub repo: Option<Repository>, + pub forge: Option<Project>, + pub default_branch: String, +} + +impl Repo { + pub fn is_clean(&self) -> Result<bool, RepoError> { + if let Some(repo) = &self.repo { + debug!("repo state: {:?}", repo.state()); + let statuses: Vec<git2::Status> = repo + .statuses(None)? + .iter() + .filter_map(|status| { + if status.status().is_ignored() { + None + } else { + Some(status.status()) + } + }) + .collect(); + + debug!("got repo statuses: {:?}", statuses); + + if repo.state() == git2::RepositoryState::Clean && statuses.is_empty() { + Ok(true) + } else { + Ok(false) + } + } else { + Err(RepoError::NoLocalRepo) + } + } + + pub fn main_remote<'a>(&self, repo: &'a Repository) -> Result<git2::Remote<'a>, RepoError> { + let remotes = repo.remotes()?; + + let remote = if remotes.iter().any(|x| x == Some("origin")) { + "origin" + } else if let Some(remote) = remotes.get(0) { + remote + } else { + return Err(RepoError::NoRemoteFound); + }; + + return Ok(repo.find_remote(remote)?); + } + + #[tracing::instrument(level = "trace", skip(remote))] + pub fn fetch<'a>(&self, remote: &mut Remote) -> Result<(), RepoError> { + // Pass an empty array as the refspec to fetch to fetch the "default" refspecs + // Type annotation is needed because type can't be guessed from the empty array + remote.fetch::<&str>( + &[], + Some(&mut crate::git::fetch_options()), + Some("gtree fetch"), + )?; + + Ok(()) + } + + #[tracing::instrument(level = "trace")] + pub fn clone(&self, url: &str) -> Result<Repository, RepoError> { + let mut builder = git2::build::RepoBuilder::new(); + builder.fetch_options(crate::git::fetch_options()); + + builder + .clone(url, &self.path) + .map_err(RepoError::GitError) + } + + #[tracing::instrument(level = "trace")] + pub fn checkout(&self) -> Result<(), RepoError> { + if let Some(repo) = &self.repo { + repo.checkout_head(None).map_err(|e| e.into()) + } else { + Err(RepoError::NoLocalRepo) + } + } + + pub fn branch_name(branch: &Branch) -> String { + match branch.name().unwrap() { + Some(s) => s.to_string(), + None => String::from_utf8_lossy(branch.name_bytes().unwrap()).to_string(), + } + } + + #[tracing::instrument(level = "trace", skip(repo, local, upstream))] + pub fn merge( + &self, + repo: &Repository, + local: &mut Branch, + upstream: &Branch, + ) -> Result<bool, RepoError> { + let local_name = Repo::branch_name(local); + let upstream_name = Repo::branch_name(upstream); + + let local_ref = local.get_mut(); + let upstream_ref = upstream.get(); + + let analysis = repo.merge_analysis_for_ref( + local_ref, + &[&repo.reference_to_annotated_commit(upstream_ref)?], + )?; + + if analysis.0.is_fast_forward() { + trace!("Doing a fast forward"); + + let msg = format!( + "gtree: update repo branch: {} to {}", + local_name, upstream_name + ); + debug!("{}", msg); + + // sets the branch to target the new commit + // of the remote branch it's tracking + local_ref.set_target(upstream_ref.target().unwrap(), &msg)?; + // Apply these changes in the working dir if the branch is currently checked out. + if format!("refs/heads/{}", local_name) == self.default_branch { + repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))? + } + + Ok(true) + } else if analysis.0.is_up_to_date() { + Ok(false) + } else { + Err(RepoError::NoFF) + } + } +} + +#[derive(Error, Debug)] +pub enum RepoError { + #[error("repo is not cloned locally")] + NoLocalRepo, + #[error("local git repo does not have a remote")] + NoRemoteFound, + #[error("repository is dirty")] + Dirty, + #[error("fast-forward merge was not possible")] + NoFF, + #[error("error during git operation {0}")] + GitError(#[from] git2::Error), + #[error("unknown repo error")] + Unknown, +} + +impl Ord for Repo { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name.cmp(&other.name) + } +} + +impl Eq for Repo {} + +impl PartialOrd for Repo { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + self.name.partial_cmp(&other.name) + } +} + +impl PartialEq for Repo { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl From<Project> for Repo { + fn from(project: Project) -> Self { + Self { + name: project.path.clone(), + forge: Some(project), + ..Repo::default() + } + } +} + +impl From<&Project> for Repo { + fn from(project: &Project) -> Self { + Self { + name: project.path.clone(), + forge: Some(project.to_owned()), + ..Repo::default() + } + } +} + +impl std::fmt::Display for Repo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {}", RepoState::from(self), self.name)) + } +} + +impl Debug for Repo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Repo").field("path", &self.name).finish() + } +} + +impl Default for Repo { + fn default() -> Self { + Self { + name: Default::default(), + path: Default::default(), + repo: Default::default(), + forge: Default::default(), + default_branch: "main".to_string(), + } + } +} diff --git a/src/repo/repostate.rs b/src/repo/repostate.rs new file mode 100644 index 0000000..ea3c5a6 --- /dev/null +++ b/src/repo/repostate.rs @@ -0,0 +1,36 @@ +use super::Repo; + +#[derive(Clone, Debug)] +pub enum RepoState { + Local, + Remote, + Synced, + Unknown, +} + +impl std::fmt::Display for RepoState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use ansi_term::Colour::{Blue, Green, Red, Yellow}; + + match self { + RepoState::Local => f.write_str(&Yellow.paint("LOCAL ").to_string()), + RepoState::Remote => f.write_str(&Blue.paint("REMOTE ").to_string()), + RepoState::Synced => f.write_str(&Green.paint("SYNCED ").to_string()), + RepoState::Unknown => f.write_str(&Red.paint("UNKNOWN").to_string()), + } + } +} + +impl From<&Repo> for RepoState { + fn from(repo: &Repo) -> Self { + if repo.repo.is_some() && repo.forge.is_some() { + RepoState::Synced + } else if repo.repo.is_some() { + RepoState::Local + } else if repo.forge.is_some() { + RepoState::Remote + } else { + RepoState::Unknown + } + } +} |
