aboutsummaryrefslogtreecommitdiff
path: root/src/local
diff options
context:
space:
mode:
Diffstat (limited to 'src/local')
-rw-r--r--src/local/;a211
-rw-r--r--src/local/aggregate.rs107
-rw-r--r--src/local/mod.rs236
-rw-r--r--src/local/repostate.rs36
-rw-r--r--src/local/sync.rs115
-rw-r--r--src/local/update.rs120
6 files changed, 0 insertions, 825 deletions
diff --git a/src/local/;a b/src/local/;a
deleted file mode 100644
index 9d6ada5..0000000
--- a/src/local/;a
+++ /dev/null
@@ -1,211 +0,0 @@
-use std::{fmt::Debug, path::PathBuf};
-
-use thiserror::Error;
-
-use git2::{AnnotatedCommit, 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 repo: Option<Repository>,
- pub forge: Option<Project>,
-}
-
-impl Repo {
- /// Fetch any new state from the remote,
- /// we get the default branch in the same run.
- ///
- /// Then check if the repo is to be considered clean,
- /// no stale uncommitted changes, no in progress merges etc
- #[tracing::instrument(level = "trace")]
- pub fn update(&self) -> Result<(), RepoError> {
- let mut remote = self.main_remote()?;
-
- let fetch_head = self.fetch(&mut remote)?;
-
- let default_branch = remote.default_branch()?.as_str().unwrap().to_string();
- debug!("default branch: {}", default_branch);
-
- if self.is_clean()? {
- debug!("repo is clean");
-
- self.merge(&default_branch, &fetch_head)?;
- };
-
- Ok(())
- }
-
- 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(&self) -> Result<git2::Remote, RepoError> {
- if let Some(repo) = &self.repo {
- let remotes = repo.remotes()?;
-
- let remote = if let Some(_) = remotes.iter().find(|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)?);
- } else {
- return Err(RepoError::NoLocalRepo);
- }
- }
-
- #[tracing::instrument(level = "trace", skip(remote))]
- pub fn fetch(&self, remote: &mut Remote) -> Result<AnnotatedCommit, 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"),
- )?;
-
- let repo = self.repo.as_ref().unwrap();
-
- let fetch_head = repo.find_reference("FETCH_HEAD")?;
-
- Ok(repo.reference_to_annotated_commit(&fetch_head)?)
- // Ok(remote.default_branch()?.as_str().unwrap().to_string())
- }
-
- 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 merge(&self, refname: &str, fetch_commit: &AnnotatedCommit) -> Result<(), RepoError> {
- let repo = self.repo.as_ref().unwrap();
-
- let analysis = repo.merge_analysis(&[fetch_commit])?;
-
- if analysis.0.is_fast_forward() {
- trace!("Doing a fast forward");
- match repo.find_reference(&refname) {
- Ok(mut r) => {
- let name = match r.name() {
- Some(s) => s.to_string(),
- None => String::from_utf8_lossy(r.name_bytes()).to_string(),
- };
- let msg = format!("gtree: update repo branch: {} to {}", name, fetch_commit.id());
- debug!("{}", msg);
-
- r.set_target(fetch_commit.id(), &msg)?;
- repo.set_head(&name)?;
- repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
- }
- Err(_) => (),
- };
- }
-
- Ok(())
- }
-}
-
-#[derive(Error, Debug)]
-pub enum RepoError {
- #[error("repo is not cloned locally")]
- NoLocalRepo,
- #[error("local git repo does not have a remote")]
- NoRemoteFound,
- #[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(),
- repo: None,
- forge: Some(project),
- }
- }
-}
-
-impl From<&Project> for Repo {
- fn from(project: &Project) -> Self {
- Self {
- name: project.path.clone(),
- repo: None,
- forge: Some(project.to_owned()),
- }
- }
-}
-
-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()
- }
-}
diff --git a/src/local/aggregate.rs b/src/local/aggregate.rs
deleted file mode 100644
index 21a1dad..0000000
--- a/src/local/aggregate.rs
+++ /dev/null
@@ -1,107 +0,0 @@
-use std::path::PathBuf;
-
-use git2::Repository;
-
-use tracing::{debug, 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 let Some(_) = dir.find(|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();
- return 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/local/mod.rs b/src/local/mod.rs
deleted file mode 100644
index ec8985b..0000000
--- a/src/local/mod.rs
+++ /dev/null
@@ -1,236 +0,0 @@
-use std::{
- fmt::Debug,
- path::{Path, PathBuf},
-};
-
-use thiserror::Error;
-
-use git2::{AnnotatedCommit, Branch, BranchType, Remote, Repository};
-use tracing::{debug, trace};
-
-use crate::forge::Project;
-
-mod aggregate;
-mod repostate;
-mod sync;
-mod update;
-
-pub use aggregate::*;
-pub use repostate::*;
-pub use sync::*;
-pub use update::*;
-
-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 let Some(_) = remotes.iter().find(|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(|err| RepoError::GitError(err))
- }
-
- #[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/local/repostate.rs b/src/local/repostate.rs
deleted file mode 100644
index ea3c5a6..0000000
--- a/src/local/repostate.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-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
- }
- }
-}
diff --git a/src/local/sync.rs b/src/local/sync.rs
deleted file mode 100644
index 0ee1b59..0000000
--- a/src/local/sync.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use std::fmt::{Debug, Display};
-
-use git2::{AnnotatedCommit, Branch, BranchType, Remote, Repository};
-use tracing::{debug, debug_span};
-
-use super::{Repo, RepoError};
-
-impl Repo {
- /// Clone repos from forge and push new repos to forge
- #[tracing::instrument(level = "trace")]
- pub fn sync(&mut self) -> Result<SyncResult, SyncResult> {
- let repo_name = self.name.clone();
-
- if self.repo.is_some()
- && !self
- .is_clean()
- .map_err(|err| SyncResult::err(repo_name.clone(), err))?
- {
- return Ok(SyncResult::dirty(repo_name));
- };
-
- if self.repo.is_some() && self.forge.is_some() {
- Ok(SyncResult::no_changes(repo_name))
- } else if self.repo.is_some() {
- // do push stuff
- Ok(SyncResult::pushed(repo_name))
- } else if self.forge.is_some() {
- let url = self
- .forge
- .as_ref()
- .unwrap()
- .ssh_clone_url
- .as_ref()
- .ok_or(SyncResult::err(self.name.clone(), RepoError::NoRemoteFound))?;
-
- let repo = self
- .clone(&url)
- .map_err(|err| SyncResult::err(repo_name.clone(), err))?;
-
- self.repo = Some(repo);
- Ok(SyncResult::cloned(repo_name))
- } else {
- Ok(SyncResult::no_changes(repo_name))
- }
- }
-}
-
-#[derive(Debug)]
-pub enum SyncResult {
- NoChanges {
- name: String,
- },
- Dirty {
- name: String,
- },
- Cloned {
- name: String,
- },
- Pushed {
- name: String,
- },
- Error {
- name: String,
- error: super::RepoError,
- },
-}
-
-impl SyncResult {
- pub fn err(name: String, error: super::RepoError) -> SyncResult {
- SyncResult::Error { name, error }
- }
-
- pub fn cloned(name: String) -> SyncResult {
- SyncResult::Cloned { name }
- }
-
- pub fn pushed(name: String) -> SyncResult {
- SyncResult::Pushed { name }
- }
-
- pub fn dirty(name: String) -> SyncResult {
- SyncResult::Dirty { name }
- }
-
- pub fn no_changes(name: String) -> SyncResult {
- SyncResult::NoChanges { name }
- }
-}
-
-impl Display for SyncResult {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- use ansi_term::Colour::{Blue, Green, Red, Yellow};
-
- match self {
- SyncResult::NoChanges { name } => {
- f.write_fmt(format_args!("{} {}", Blue.paint("NOCHANGE"), name))
- }
- SyncResult::Dirty { name } => {
- f.write_fmt(format_args!("{} {}", Yellow.paint("DIRTY "), name))
- }
- SyncResult::Cloned { name } => {
- f.write_fmt(format_args!("{} {}", Green.paint("CLONED "), name))
- }
- SyncResult::Pushed { name } => {
- f.write_fmt(format_args!("{} {}", Green.paint("PUSHED "), name))
- }
- SyncResult::Error { name, error } => f.write_fmt(format_args!(
- "{} {} [{}]",
- Red.paint("ERROR "),
- name,
- error
- )),
- }
- }
-}
diff --git a/src/local/update.rs b/src/local/update.rs
deleted file mode 100644
index d9c9f3c..0000000
--- a/src/local/update.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-use std::fmt::{Debug, Display};
-
-use git2::BranchType;
-use tracing::debug;
-
-use super::{Repo, RepoError};
-
-impl Repo {
- /// Fetch any new state from the remote and fast forward merge changes into local branches
- #[tracing::instrument(level = "trace")]
- pub fn update(&mut self) -> Result<UpdateResult, UpdateResult> {
- let repo_name = self.name.clone();
- if self.repo.is_some() {
- self.update_inner()
- .map_err(|e| UpdateResult::err(repo_name, e.into()))
- } else {
- Ok(UpdateResult::err(repo_name, RepoError::NoLocalRepo))
- }
- }
-
- fn update_inner(&mut self) -> Result<UpdateResult, RepoError> {
- let repo = self.repo.as_ref().unwrap();
- let mut remote = self.main_remote(repo)?;
-
- self.fetch(&mut remote)?;
-
- self.default_branch = remote.default_branch()?.as_str().unwrap().to_string();
-
- debug!("default branch: {}", self.default_branch);
-
- if self.is_clean()? {
- debug!("repo is clean");
-
- let merged = repo.branches(Some(BranchType::Local))?
- .filter_map(|x| x.ok())
- .try_fold(false, |mut merged, (mut branch, _)| {
- let name = format!("refs/heads/{}", Repo::branch_name(&branch));
-
- if branch.upstream().is_ok() {
- let upstream = branch.upstream().unwrap();
-
- debug!("branch: {}", name);
-
- merged |= self.merge(repo, &mut branch, &upstream)?;
- Ok::<bool, RepoError>(merged)
- } else {
- debug!("not updating branch: {}: branch does not have upstream tracking branch set", name);
- Ok(merged)
- }
- })?;
-
- if merged {
- Ok(UpdateResult::merged(self.name.clone()))
- } else {
- Ok(UpdateResult::no_changes(self.name.clone()))
- }
- } else {
- Ok(UpdateResult::dirty(self.name.clone()))
- }
- }
-}
-
-#[derive(Debug)]
-pub enum UpdateResult {
- NoChanges {
- name: String,
- },
- Dirty {
- name: String,
- },
- Merged {
- name: String,
- },
- Error {
- name: String,
- error: super::RepoError,
- },
-}
-
-impl UpdateResult {
- pub fn err(name: String, error: super::RepoError) -> UpdateResult {
- UpdateResult::Error { name, error }
- }
-
- pub fn merged(name: String) -> UpdateResult {
- UpdateResult::Merged { name }
- }
-
- pub fn dirty(name: String) -> UpdateResult {
- UpdateResult::Dirty { name }
- }
-
- pub fn no_changes(name: String) -> UpdateResult {
- UpdateResult::NoChanges { name }
- }
-}
-
-impl Display for UpdateResult {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- use ansi_term::Colour::{Blue, Green, Red, Yellow};
-
- match self {
- UpdateResult::NoChanges { name } => {
- f.write_fmt(format_args!("{} {}", Blue.paint("FETCHED"), name))
- }
- UpdateResult::Dirty { name } => {
- f.write_fmt(format_args!("{} {}", Yellow.paint("DIRTY "), name))
- }
- UpdateResult::Merged { name } => {
- f.write_fmt(format_args!("{} {}", Green.paint("PULLED "), name))
- }
- UpdateResult::Error { name, error } => f.write_fmt(format_args!(
- "{} {} [{}]",
- Red.paint("ERROR "),
- name,
- error
- )),
- }
- }
-}