aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Audron <audron@cocaine.farm>2022-06-07 12:28:18 +0200
committerMaximilian Manz <maximilian.manz@de.clara.net>2022-06-20 11:33:04 +0200
commite9dc01ffb547d0fa605bfe38b34672ddd5161be4 (patch)
tree5ca50547512b7cc2256ef457d468c4252ae23a0b
parentimplement cloning of new repos (diff)
reorganize file structure and cleanup lints
-rw-r--r--src/config/args.rs9
-rw-r--r--src/config/mod.rs6
-rw-r--r--src/forge/gitlab/mod.rs19
-rw-r--r--src/forge/mod.rs3
-rw-r--r--src/lib.rs4
-rw-r--r--src/list/mod.rs9
-rw-r--r--src/local/;a211
-rw-r--r--src/local/sync.rs115
-rw-r--r--src/local/update.rs120
-rw-r--r--src/main.rs24
-rw-r--r--src/repo/aggregate.rs (renamed from src/local/aggregate.rs)8
-rw-r--r--src/repo/mod.rs (renamed from src/local/mod.rs)29
-rw-r--r--src/repo/repostate.rs (renamed from src/local/repostate.rs)0
-rw-r--r--src/sync/mod.rs102
-rw-r--r--src/tests/mod.rs (renamed from tests/directory_walker.rs)24
-rw-r--r--src/update/mod.rs114
16 files changed, 274 insertions, 523 deletions
diff --git a/src/config/args.rs b/src/config/args.rs
index 9c0973f..446c5ce 100644
--- a/src/config/args.rs
+++ b/src/config/args.rs
@@ -1,9 +1,7 @@
-use clap::{Parser, Subcommand, ArgEnum};
+use clap::{Parser, Subcommand};
#[derive(Parser, Clone, Debug)]
-#[clap(
- override_usage("gtree <SUBCOMMAND> [SCOPE]")
-)]
+#[clap(override_usage("gtree <SUBCOMMAND> [SCOPE]"))]
/// Sync Gitlab Trees
pub struct Args {
#[clap(subcommand)]
@@ -14,8 +12,7 @@ pub struct Args {
pub scope: Option<String>,
}
-#[derive(PartialEq, Clone, Debug)]
-#[derive(Subcommand)]
+#[derive(PartialEq, Clone, Debug, Subcommand)]
pub enum Commands {
/// Download new repositories and delete old ones, also update
Sync,
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 26d80f4..781aedf 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -2,7 +2,7 @@ pub mod args;
use serde::{Deserialize, Serialize};
-use std::{collections::BTreeMap, ops::Deref, path::Path};
+use std::{collections::BTreeMap, ops::Deref};
use figment::{
providers::{Format, Toml},
@@ -19,7 +19,7 @@ use anyhow::{Context, Result};
// TODO make forge optional
pub struct Config {
#[serde(flatten)]
- config: BTreeMap<String, ForgeConfig>
+ config: BTreeMap<String, ForgeConfig>,
}
impl Deref for Config {
@@ -32,7 +32,7 @@ impl Deref for Config {
impl Config {
// Allow the configuration to be extracted from any `Provider`.
- fn from<T: Provider>(provider: T) -> Result<Config, Error> {
+ pub fn from<T: Provider>(provider: T) -> Result<Config, Error> {
Figment::from(provider).extract()
}
diff --git a/src/forge/gitlab/mod.rs b/src/forge/gitlab/mod.rs
index 85efc76..fac60b7 100644
--- a/src/forge/gitlab/mod.rs
+++ b/src/forge/gitlab/mod.rs
@@ -1,8 +1,7 @@
-use anyhow::{bail, Context, Result};
+use anyhow::Result;
use gitlab::AsyncGitlab;
use graphql_client::GraphQLQuery;
-use tracing::{debug, trace};
pub mod config;
@@ -58,18 +57,18 @@ impl super::ForgeTrait for Gitlab {
query_path = "graphql/projects_query.graphql",
schema_path = "graphql/schema.graphql",
response_derives = "Clone,Debug",
- variables_derives = "Clone,Debug",
+ variables_derives = "Clone,Debug"
)]
pub struct Projects;
-impl Into<super::Project> for projects::ProjectsProjectsNodes {
- fn into(self) -> super::Project {
+impl From<projects::ProjectsProjectsNodes> for super::Project {
+ fn from(project: projects::ProjectsProjectsNodes) -> Self {
super::Project {
- id: self.id,
- name: self.name,
- path: self.full_path,
- ssh_clone_url: self.ssh_url_to_repo,
- http_clone_url: self.http_url_to_repo,
+ id: project.id,
+ name: project.name,
+ path: project.full_path,
+ ssh_clone_url: project.ssh_url_to_repo,
+ http_clone_url: project.http_url_to_repo,
}
}
}
diff --git a/src/forge/mod.rs b/src/forge/mod.rs
index 3e94812..5a78850 100644
--- a/src/forge/mod.rs
+++ b/src/forge/mod.rs
@@ -1,6 +1,6 @@
use std::ops::Deref;
-use anyhow::{Result, bail};
+use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use crate::config::ForgeConfig;
@@ -18,6 +18,7 @@ impl Forge {
ForgeConfig::Gitlab(config) => {
Ok(Forge::Gitlab(gitlab::Gitlab::from_config(config).await?))
}
+ #[allow(unreachable_patterns)]
_ => bail!("wrong forge type found"),
}
}
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index 7039db7..0000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-pub mod config;
-pub mod forge;
-pub mod local;
-pub mod git;
diff --git a/src/list/mod.rs b/src/list/mod.rs
index e05a318..ed71e6a 100644
--- a/src/list/mod.rs
+++ b/src/list/mod.rs
@@ -1,11 +1,10 @@
-use anyhow::{Context, Result};
-use gtree::local::Repos;
+use anyhow::Result;
+
+use crate::repo::Repos;
impl crate::GTree {
pub async fn list(&self, repos: Repos) -> Result<()> {
- repos.iter().for_each(|repo| {
- println!("{}", repo)
- });
+ repos.iter().for_each(|repo| println!("{}", repo));
Ok(())
}
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/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
- )),
- }
- }
-}
diff --git a/src/main.rs b/src/main.rs
index 97929af..eeec4a3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,27 +2,30 @@ use anyhow::{Context, Result};
use clap::Parser;
use derivative::Derivative;
-use tracing::{debug, info, metadata::LevelFilter, trace, Level};
+use tracing::{debug, metadata::LevelFilter, Level};
use tracing_subscriber::{fmt::format::FmtSpan, prelude::*, EnvFilter};
-use gtree::{
- config,
- local::{Aggregator, Repos},
-};
+use crate::repo::{Aggregator, Repos};
+
+pub mod config;
+pub mod forge;
+pub mod git;
+pub mod repo;
mod list;
mod sync;
mod update;
+#[cfg(test)]
+mod tests;
+
#[derive(Derivative)]
#[derivative(Debug)]
struct GTree {
figment: figment::Figment,
config: config::Config,
args: config::args::Args,
- forge: gtree::forge::Forge,
- #[derivative(Debug = "ignore")]
- gitconfig: git2::Config,
+ forge: forge::Forge,
}
impl GTree {
@@ -38,16 +41,13 @@ impl GTree {
.next()
.context("No Forge configured, please setup a forge")?;
- let forge = gtree::forge::Forge::new(forge_config).await?;
-
- let gitconfig = git2::Config::open_default()?;
+ let forge = forge::Forge::new(forge_config).await?;
Ok(GTree {
figment,
config,
args,
forge,
- gitconfig,
})
}
diff --git a/src/local/aggregate.rs b/src/repo/aggregate.rs
index 21a1dad..cb4b00d 100644
--- a/src/local/aggregate.rs
+++ b/src/repo/aggregate.rs
@@ -1,8 +1,6 @@
-use std::path::PathBuf;
-
use git2::Repository;
-use tracing::{debug, error};
+use tracing::error;
use walkdir::WalkDir;
use crate::forge::Project;
@@ -40,7 +38,7 @@ impl Aggregator for Repos {
if entry.file_type().is_dir() {
let mut dir = std::fs::read_dir(entry.path()).unwrap();
- if let Some(_) = dir.find(|dir| {
+ if dir.any(|dir| {
if let Ok(dir) = dir {
dir.file_name() == ".git"
} else {
@@ -80,7 +78,7 @@ impl Aggregator for Repos {
.map(|project| {
let mut repo: Repo = project.into();
repo.path = [root, &repo.name].iter().collect();
- return repo;
+ repo
})
.collect()
}
diff --git a/src/local/mod.rs b/src/repo/mod.rs
index ec8985b..e3d1279 100644
--- a/src/local/mod.rs
+++ b/src/repo/mod.rs
@@ -1,24 +1,17 @@
-use std::{
- fmt::Debug,
- path::{Path, PathBuf},
-};
+use std::{fmt::Debug, path::PathBuf};
use thiserror::Error;
-use git2::{AnnotatedCommit, Branch, BranchType, Remote, Repository};
+use git2::{Branch, 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>;
@@ -61,14 +54,12 @@ impl Repo {
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")) {
+ let remote = if remotes.iter().any(|x| x == Some("origin")) {
"origin"
+ } else if let Some(remote) = remotes.get(0) {
+ remote
} else {
- if let Some(remote) = remotes.get(0) {
- remote
- } else {
- return Err(RepoError::NoRemoteFound);
- }
+ return Err(RepoError::NoRemoteFound);
};
return Ok(repo.find_remote(remote)?);
@@ -92,7 +83,9 @@ impl Repo {
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))
+ builder
+ .clone(url, &self.path)
+ .map_err(RepoError::GitError)
}
#[tracing::instrument(level = "trace")]
@@ -118,8 +111,8 @@ impl Repo {
local: &mut Branch,
upstream: &Branch,
) -> Result<bool, RepoError> {
- let local_name = Repo::branch_name(&local);
- let upstream_name = Repo::branch_name(&upstream);
+ 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();
diff --git a/src/local/repostate.rs b/src/repo/repostate.rs
index ea3c5a6..ea3c5a6 100644
--- a/src/local/repostate.rs
+++ b/src/repo/repostate.rs
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index 2edc04c..f707b04 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -1,4 +1,6 @@
-use gtree::local::Repos;
+use std::fmt::{Debug, Display};
+
+use crate::repo::{Repo, RepoError, Repos};
impl crate::GTree {
pub async fn sync(&self, repos: Repos) {
@@ -10,3 +12,101 @@ impl crate::GTree {
}
}
}
+
+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_else(|| 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: RepoError },
+}
+
+impl SyncResult {
+ pub fn err(name: String, error: 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/tests/directory_walker.rs b/src/tests/mod.rs
index 9fa8d56..0d32dbf 100644
--- a/tests/directory_walker.rs
+++ b/src/tests/mod.rs
@@ -1,9 +1,13 @@
-use gtree::local::*;
+use crate::repo::*;
use anyhow::Result;
use git2::Repository;
-const TEST_DIR: &str = env!("CARGO_TARGET_TMPDIR");
+thread_local! {
+ static TEST_DIR: std::path::PathBuf = std::env::current_exe()
+ .unwrap()
+ .join(std::path::Path::new("../../tmp"));
+}
const REPOS: [&str; 5] = [
"repos/site/group/repo1",
@@ -15,7 +19,7 @@ const REPOS: [&str; 5] = [
fn prepare_repos() -> Result<()> {
REPOS.iter().try_for_each(|repo| {
- let path = format!("{}/{}", TEST_DIR, repo);
+ let path = format!("{:?}/{}", TEST_DIR, repo);
std::fs::create_dir_all(&path)?;
let _repo = Repository::init(&path)?;
@@ -25,7 +29,7 @@ fn prepare_repos() -> Result<()> {
fn clean_repos() -> Result<()> {
REPOS.iter().try_for_each(|repo| {
- let path = format!("{}/{}", TEST_DIR, repo);
+ let path = format!("{:?}/{}", TEST_DIR, repo);
std::fs::remove_dir_all(&path)?;
Ok::<(), anyhow::Error>(())
@@ -39,16 +43,16 @@ async fn search_repos() -> Result<()> {
prepare_repos()?;
let mut left: Vec<String> = vec![
- format!("{}/repos/site/group/repo1", TEST_DIR),
- format!("{}/repos/site/group/repo2", TEST_DIR),
- format!("{}/repos/site/group/subgroup/repo3", TEST_DIR),
- format!("{}/repos/site/group/subgroup/subsubgroup/repo4", TEST_DIR),
+ format!("{:?}/repos/site/group/repo1", TEST_DIR),
+ format!("{:?}/repos/site/group/repo2", TEST_DIR),
+ format!("{:?}/repos/site/group/subgroup/repo3", TEST_DIR),
+ format!("{:?}/repos/site/group/subgroup/subsubgroup/repo4", TEST_DIR),
];
- let right = Repos::from_local(&format!("{}/repos", TEST_DIR), "").await;
+ let right = Repos::from_local(&format!("{:?}/repos", TEST_DIR), "").await;
let mut right: Vec<&str> = right.iter().map(|x| x.name.as_str()).collect();
- assert_eq!(left.sort(), right.sort());
+ assert_eq!(left.sort(), right.sort_unstable());
clean_repos()?;
Ok(())
diff --git a/src/update/mod.rs b/src/update/mod.rs
index 0991b1a..dfc800c 100644
--- a/src/update/mod.rs
+++ b/src/update/mod.rs
@@ -1,9 +1,14 @@
-use gtree::local::Repos;
+use std::fmt::{Debug, Display};
+
+use git2::BranchType;
+use tracing::debug;
+
+use crate::repo::{Repo, RepoError, Repos};
impl crate::GTree {
pub async fn update(&self, repos: Repos) {
for mut repo in repos {
- if let Some(_) = repo.repo {
+ if repo.repo.is_some() {
match repo.update() {
Ok(u) => println!("{}", u),
Err(u) => println!("{}", u),
@@ -12,3 +17,108 @@ impl crate::GTree {
}
}
}
+
+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))
+ } 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: RepoError },
+}
+
+impl UpdateResult {
+ pub fn err(name: String, error: 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
+ )),
+ }
+ }
+}