diff --git a/BRANCH_NAMESPACE_SEPARATOR b/BRANCH_NAMESPACE_SEPARATOR new file mode 100644 index 0000000..e69de29 diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..d4a79a7 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,36 @@ +use std::process; + +pub fn get_token_from_command(command: &str) -> Result { + let output = process::Command::new("/usr/bin/env") + .arg("sh") + .arg("-c") + .arg(command) + .output() + .map_err(|error| format!("Failed to run token-command: {}", error))?; + + let stderr = String::from_utf8(output.stderr).map_err(|error| error.to_string())?; + let stdout = String::from_utf8(output.stdout).map_err(|error| error.to_string())?; + + if !output.status.success() { + if !stderr.is_empty() { + return Err(format!("Token command failed: {}", stderr)); + } else { + return Err(String::from("Token command failed.")); + } + } + + if !stderr.is_empty() { + return Err(format!("Token command produced stderr: {}", stderr)); + } + + if stdout.is_empty() { + return Err(String::from("Token command did not produce output")); + } + + let token = stdout + .split('\n') + .next() + .ok_or_else(|| String::from("Output did not contain any newline"))?; + + Ok(token.to_string()) +} diff --git a/src/config.rs b/src/config.rs index bbead78..f8e9464 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,18 +1,19 @@ use serde::{Deserialize, Serialize}; use std::process; -use crate::output::*; - use std::path::Path; -use crate::{get_token_from_command, path_as_string, Remote, Repo, Tree}; +use super::auth; +use super::output::*; +use super::path; +use super::provider; +use super::provider::Filter; +use super::provider::Provider; +use super::repo; +use super::tree; -use crate::provider; -use crate::provider::Filter; -use crate::provider::Provider; - -pub type RemoteProvider = crate::provider::RemoteProvider; -pub type RemoteType = crate::repo::RemoteType; +pub type RemoteProvider = provider::RemoteProvider; +pub type RemoteType = repo::RemoteType; fn worktree_setup_default() -> bool { false @@ -64,7 +65,7 @@ pub struct RemoteConfig { } impl RemoteConfig { - pub fn from_remote(remote: Remote) -> Self { + pub fn from_remote(remote: repo::Remote) -> Self { Self { name: remote.name, url: remote.url, @@ -72,8 +73,8 @@ impl RemoteConfig { } } - pub fn into_remote(self) -> Remote { - Remote { + pub fn into_remote(self) -> repo::Remote { + repo::Remote { name: self.name, url: self.url, remote_type: self.remote_type, @@ -93,7 +94,7 @@ pub struct RepoConfig { } impl RepoConfig { - pub fn from_repo(repo: Repo) -> Self { + pub fn from_repo(repo: repo::Repo) -> Self { Self { name: repo.name, worktree_setup: repo.worktree_setup, @@ -103,14 +104,14 @@ impl RepoConfig { } } - pub fn into_repo(self) -> Repo { + pub fn into_repo(self) -> repo::Repo { let (namespace, name) = if let Some((namespace, name)) = self.name.rsplit_once('/') { (Some(namespace.to_string()), name.to_string()) } else { (None, self.name) }; - Repo { + repo::Repo { name, namespace, worktree_setup: self.worktree_setup, @@ -133,7 +134,7 @@ impl ConfigTrees { ConfigTrees { trees: vec } } - pub fn from_trees(vec: Vec) -> Self { + pub fn from_trees(vec: Vec) -> Self { ConfigTrees { trees: vec.into_iter().map(ConfigTree::from_tree).collect(), } @@ -157,7 +158,7 @@ impl Config { match self { Config::ConfigTrees(config) => Ok(config.trees), Config::ConfigProvider(config) => { - let token = match get_token_from_command(&config.token_command) { + let token = match auth::get_token_from_command(&config.token_command) { Ok(token) => token, Err(error) => { print_error(&format!("Getting token from command failed: {}", error)); @@ -217,9 +218,9 @@ impl Config { .collect(); let tree = ConfigTree { root: if let Some(namespace) = namespace { - path_as_string(&Path::new(&config.root).join(namespace)) + path::path_as_string(&Path::new(&config.root).join(namespace)) } else { - path_as_string(Path::new(&config.root)) + path::path_as_string(Path::new(&config.root)) }, repos: Some(repos), }; @@ -236,7 +237,7 @@ impl Config { pub fn normalize(&mut self) { if let Config::ConfigTrees(config) = self { - let home = super::env_home().display().to_string(); + let home = path::env_home().display().to_string(); for tree in &mut config.trees_mut().iter_mut() { if tree.root.starts_with(&home) { // The tilde is not handled differently, it's just a normal path component for `Path`. @@ -275,14 +276,14 @@ pub struct ConfigTree { } impl ConfigTree { - pub fn from_repos(root: String, repos: Vec) -> Self { + pub fn from_repos(root: String, repos: Vec) -> Self { Self { root, repos: Some(repos.into_iter().map(RepoConfig::from_repo).collect()), } } - pub fn from_tree(tree: Tree) -> Self { + pub fn from_tree(tree: tree::Tree) -> Self { Self { root: tree.root, repos: Some(tree.repos.into_iter().map(RepoConfig::from_repo).collect()), diff --git a/src/grm/cmd.rs b/src/grm/cmd.rs index 89340e9..593474b 100644 --- a/src/grm/cmd.rs +++ b/src/grm/cmd.rs @@ -181,7 +181,7 @@ pub struct Config { pub init_worktree: String, } -pub type RemoteProvider = grm::provider::RemoteProvider; +pub type RemoteProvider = super::provider::RemoteProvider; #[derive(Parser)] #[clap()] diff --git a/src/grm/main.rs b/src/grm/main.rs index 630ee67..ebf9b1f 100644 --- a/src/grm/main.rs +++ b/src/grm/main.rs @@ -3,12 +3,17 @@ use std::process; mod cmd; +use grm::auth; use grm::config; +use grm::find_in_tree; use grm::output::*; -use grm::path_as_string; +use grm::path; use grm::provider; use grm::provider::Provider; use grm::repo; +use grm::table; +use grm::tree; +use grm::worktree; fn main() { let opts = cmd::parse(); @@ -24,7 +29,7 @@ fn main() { process::exit(1); } }; - match grm::sync_trees(config, args.init_worktree == "true") { + match tree::sync_trees(config, args.init_worktree == "true") { Ok(success) => { if !success { process::exit(1) @@ -37,7 +42,7 @@ fn main() { } } cmd::SyncAction::Remote(args) => { - let token = match grm::get_token_from_command(&args.token_command) { + let token = match auth::get_token_from_command(&args.token_command) { Ok(token) => token, Err(error) => { print_error(&format!("Getting token from command failed: {}", error)); @@ -45,18 +50,14 @@ fn main() { } }; - let filter = grm::provider::Filter::new( - args.users, - args.groups, - args.owner, - args.access, - ); + let filter = + provider::Filter::new(args.users, args.groups, args.owner, args.access); let worktree = args.worktree == "true"; let repos = match args.provider { cmd::RemoteProvider::Github => { - match grm::provider::Github::new(filter, token, args.api_url) { + match provider::Github::new(filter, token, args.api_url) { Ok(provider) => provider, Err(error) => { print_error(&format!("Error: {}", error)); @@ -66,7 +67,7 @@ fn main() { .get_repos(worktree, args.force_ssh) } cmd::RemoteProvider::Gitlab => { - match grm::provider::Gitlab::new(filter, token, args.api_url) { + match provider::Gitlab::new(filter, token, args.api_url) { Ok(provider) => provider, Err(error) => { print_error(&format!("Error: {}", error)); @@ -83,9 +84,9 @@ fn main() { for (namespace, repolist) in repos { let root = if let Some(namespace) = namespace { - path_as_string(&Path::new(&args.root).join(namespace)) + path::path_as_string(&Path::new(&args.root).join(namespace)) } else { - path_as_string(Path::new(&args.root)) + path::path_as_string(Path::new(&args.root)) }; let tree = config::ConfigTree::from_repos(root, repolist); @@ -94,7 +95,7 @@ fn main() { let config = config::Config::from_trees(trees); - match grm::sync_trees(config, args.init_worktree == "true") { + match tree::sync_trees(config, args.init_worktree == "true") { Ok(success) => { if !success { process::exit(1) @@ -122,7 +123,7 @@ fn main() { process::exit(1); } }; - match grm::table::get_status_table(config) { + match table::get_status_table(config) { Ok((tables, errors)) => { for table in tables { println!("{}", table); @@ -146,7 +147,7 @@ fn main() { } }; - match grm::table::show_single_repo_status(&dir) { + match table::show_single_repo_status(&dir) { Ok((table, warnings)) => { println!("{}", table); for warning in warnings { @@ -184,7 +185,7 @@ fn main() { } }; - let (found_repos, warnings) = match grm::find_in_tree(&path) { + let (found_repos, warnings) = match find_in_tree(&path) { Ok((repos, warnings)) => (repos, warnings), Err(error) => { print_error(&error); @@ -192,7 +193,7 @@ fn main() { } }; - let trees = grm::config::ConfigTrees::from_trees(vec![found_repos]); + let trees = config::ConfigTrees::from_trees(vec![found_repos]); if trees.trees_ref().iter().all(|t| match &t.repos { None => false, Some(r) => r.is_empty(), @@ -237,16 +238,15 @@ fn main() { } } cmd::FindAction::Config(args) => { - let config: crate::config::ConfigProvider = - match config::read_config(&args.config) { - Ok(config) => config, - Err(error) => { - print_error(&error); - process::exit(1); - } - }; + let config: config::ConfigProvider = match config::read_config(&args.config) { + Ok(config) => config, + Err(error) => { + print_error(&error); + process::exit(1); + } + }; - let token = match grm::get_token_from_command(&config.token_command) { + let token = match auth::get_token_from_command(&config.token_command) { Ok(token) => token, Err(error) => { print_error(&format!("Getting token from command failed: {}", error)); @@ -254,7 +254,7 @@ fn main() { } }; - let filters = config.filters.unwrap_or(grm::config::ConfigProviderFilter { + let filters = config.filters.unwrap_or(config::ConfigProviderFilter { access: Some(false), owner: Some(false), users: Some(vec![]), @@ -314,14 +314,14 @@ fn main() { for (namespace, namespace_repos) in repos { let tree = config::ConfigTree { root: if let Some(namespace) = namespace { - path_as_string(&Path::new(&config.root).join(namespace)) + path::path_as_string(&Path::new(&config.root).join(namespace)) } else { - path_as_string(Path::new(&config.root)) + path::path_as_string(Path::new(&config.root)) }, repos: Some( namespace_repos .into_iter() - .map(grm::config::RepoConfig::from_repo) + .map(config::RepoConfig::from_repo) .collect(), ), }; @@ -360,7 +360,7 @@ fn main() { } } cmd::FindAction::Remote(args) => { - let token = match grm::get_token_from_command(&args.token_command) { + let token = match auth::get_token_from_command(&args.token_command) { Ok(token) => token, Err(error) => { print_error(&format!("Getting token from command failed: {}", error)); @@ -368,18 +368,14 @@ fn main() { } }; - let filter = grm::provider::Filter::new( - args.users, - args.groups, - args.owner, - args.access, - ); + let filter = + provider::Filter::new(args.users, args.groups, args.owner, args.access); let worktree = args.worktree == "true"; let repos = match args.provider { cmd::RemoteProvider::Github => { - match grm::provider::Github::new(filter, token, args.api_url) { + match provider::Github::new(filter, token, args.api_url) { Ok(provider) => provider, Err(error) => { print_error(&format!("Error: {}", error)); @@ -389,7 +385,7 @@ fn main() { .get_repos(worktree, args.force_ssh) } cmd::RemoteProvider::Gitlab => { - match grm::provider::Gitlab::new(filter, token, args.api_url) { + match provider::Gitlab::new(filter, token, args.api_url) { Ok(provider) => provider, Err(error) => { print_error(&format!("Error: {}", error)); @@ -410,14 +406,14 @@ fn main() { for (namespace, repolist) in repos { let tree = config::ConfigTree { root: if let Some(namespace) = namespace { - path_as_string(&Path::new(&args.root).join(namespace)) + path::path_as_string(&Path::new(&args.root).join(namespace)) } else { - path_as_string(Path::new(&args.root)) + path::path_as_string(Path::new(&args.root)) }, repos: Some( repolist .into_iter() - .map(grm::config::RepoConfig::from_repo) + .map(config::RepoConfig::from_repo) .collect(), ), }; @@ -503,7 +499,13 @@ fn main() { } } - match grm::add_worktree(&cwd, name, subdirectory, track, action_args.no_track) { + match worktree::add_worktree( + &cwd, + name, + subdirectory, + track, + action_args.no_track, + ) { Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)), Err(error) => { print_error(&format!("Error creating worktree: {}", error)); @@ -525,7 +527,7 @@ fn main() { } }; - let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { + let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { print_error(&format!("Error opening repository: {}", error)); process::exit(1); }); @@ -539,17 +541,17 @@ fn main() { Ok(_) => print_success(&format!("Worktree {} deleted", &action_args.name)), Err(error) => { match error { - grm::WorktreeRemoveFailureReason::Error(msg) => { + repo::WorktreeRemoveFailureReason::Error(msg) => { print_error(&msg); process::exit(1); } - grm::WorktreeRemoveFailureReason::Changes(changes) => { + repo::WorktreeRemoveFailureReason::Changes(changes) => { print_warning(&format!( "Changes in worktree: {}. Refusing to delete", changes )); } - grm::WorktreeRemoveFailureReason::NotMerged(message) => { + repo::WorktreeRemoveFailureReason::NotMerged(message) => { print_warning(&message); } } @@ -558,12 +560,12 @@ fn main() { } } cmd::WorktreeAction::Status(_args) => { - let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { + let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { print_error(&format!("Error opening repository: {}", error)); process::exit(1); }); - match grm::table::get_worktree_status_table(&repo, &cwd) { + match table::get_worktree_status_table(&repo, &cwd) { Ok((table, errors)) => { println!("{}", table); for error in errors { @@ -583,8 +585,8 @@ fn main() { // * Remove all files // * Set `core.bare` to `true` - let repo = grm::RepoHandle::open(&cwd, false).unwrap_or_else(|error| { - if error.kind == grm::RepoErrorKind::NotFound { + let repo = repo::RepoHandle::open(&cwd, false).unwrap_or_else(|error| { + if error.kind == repo::RepoErrorKind::NotFound { print_error("Directory does not contain a git repository"); } else { print_error(&format!("Opening repository failed: {}", error)); @@ -611,8 +613,8 @@ fn main() { } } cmd::WorktreeAction::Clean(_args) => { - let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { - if error.kind == grm::RepoErrorKind::NotFound { + let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { + if error.kind == repo::RepoErrorKind::NotFound { print_error("Directory does not contain a git repository"); } else { print_error(&format!("Opening repository failed: {}", error)); @@ -645,8 +647,8 @@ fn main() { } } cmd::WorktreeAction::Fetch(_args) => { - let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { - if error.kind == grm::RepoErrorKind::NotFound { + let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { + if error.kind == repo::RepoErrorKind::NotFound { print_error("Directory does not contain a git repository"); } else { print_error(&format!("Opening repository failed: {}", error)); @@ -661,8 +663,8 @@ fn main() { print_success("Fetched from all remotes"); } cmd::WorktreeAction::Pull(args) => { - let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { - if error.kind == grm::RepoErrorKind::NotFound { + let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { + if error.kind == repo::RepoErrorKind::NotFound { print_error("Directory does not contain a git repository"); } else { print_error(&format!("Opening repository failed: {}", error)); @@ -702,8 +704,8 @@ fn main() { print_error("There is no point in using --rebase without --pull"); process::exit(1); } - let repo = grm::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { - if error.kind == grm::RepoErrorKind::NotFound { + let repo = repo::RepoHandle::open(&cwd, true).unwrap_or_else(|error| { + if error.kind == repo::RepoErrorKind::NotFound { print_error("Directory does not contain a git repository"); } else { print_error(&format!("Opening repository failed: {}", error)); @@ -718,14 +720,10 @@ fn main() { }); } - let config = - grm::repo::read_worktree_root_config(&cwd).unwrap_or_else(|error| { - print_error(&format!( - "Failed to read worktree configuration: {}", - error - )); - process::exit(1); - }); + let config = repo::read_worktree_root_config(&cwd).unwrap_or_else(|error| { + print_error(&format!("Failed to read worktree configuration: {}", error)); + process::exit(1); + }); let worktrees = repo.get_worktrees().unwrap_or_else(|error| { print_error(&format!("Error getting worktrees: {}", error)); diff --git a/src/lib.rs b/src/lib.rs index cd05ce5..7cbf992 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,423 +1,37 @@ #![feature(io_error_more)] #![feature(const_option_ext)] -use std::fs; -use std::path::{Path, PathBuf}; -use std::process; +use std::path::Path; +pub mod auth; pub mod config; pub mod output; +pub mod path; pub mod provider; pub mod repo; pub mod table; +pub mod tree; +pub mod worktree; -use config::Config; -use output::*; - -use repo::{clone_repo, detect_remote_type, Remote, RemoteType}; - -pub use repo::{ - RemoteTrackingStatus, Repo, RepoErrorKind, RepoHandle, WorktreeRemoveFailureReason, -}; - -const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree"; const BRANCH_NAMESPACE_SEPARATOR: &str = "/"; -const GIT_CONFIG_BARE_KEY: &str = "core.bare"; -const GIT_CONFIG_PUSH_DEFAULT: &str = "push.default"; - -pub struct Tree { - root: String, - repos: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - - fn setup() { - std::env::set_var("HOME", "/home/test"); - } - - #[test] - fn check_expand_tilde() { - setup(); - assert_eq!( - expand_path(Path::new("~/file")), - Path::new("/home/test/file") - ); - } - - #[test] - fn check_expand_invalid_tilde() { - setup(); - assert_eq!( - expand_path(Path::new("/home/~/file")), - Path::new("/home/~/file") - ); - } - - #[test] - fn check_expand_home() { - setup(); - assert_eq!( - expand_path(Path::new("$HOME/file")), - Path::new("/home/test/file") - ); - assert_eq!( - expand_path(Path::new("${HOME}/file")), - Path::new("/home/test/file") - ); - } -} - -pub fn path_as_string(path: &Path) -> String { - path.to_path_buf().into_os_string().into_string().unwrap() -} - -pub fn env_home() -> PathBuf { - match std::env::var("HOME") { - Ok(path) => Path::new(&path).to_path_buf(), - Err(e) => { - print_error(&format!("Unable to read HOME: {}", e)); - process::exit(1); - } - } -} - -fn expand_path(path: &Path) -> PathBuf { - fn home_dir() -> Option { - Some(env_home()) - } - - let expanded_path = match shellexpand::full_with_context( - &path_as_string(path), - home_dir, - |name| -> Result, &'static str> { - match name { - "HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))), - _ => Ok(None), - } - }, - ) { - Ok(std::borrow::Cow::Borrowed(path)) => path.to_owned(), - Ok(std::borrow::Cow::Owned(path)) => path, - Err(e) => { - print_error(&format!("Unable to expand root: {}", e)); - process::exit(1); - } - }; - - Path::new(&expanded_path).to_path_buf() -} - -pub fn get_token_from_command(command: &str) -> Result { - let output = std::process::Command::new("/usr/bin/env") - .arg("sh") - .arg("-c") - .arg(command) - .output() - .map_err(|error| format!("Failed to run token-command: {}", error))?; - - let stderr = String::from_utf8(output.stderr).map_err(|error| error.to_string())?; - let stdout = String::from_utf8(output.stdout).map_err(|error| error.to_string())?; - - if !output.status.success() { - if !stderr.is_empty() { - return Err(format!("Token command failed: {}", stderr)); - } else { - return Err(String::from("Token command failed.")); - } - } - - if !stderr.is_empty() { - return Err(format!("Token command produced stderr: {}", stderr)); - } - - if stdout.is_empty() { - return Err(String::from("Token command did not produce output")); - } - - let token = stdout - .split('\n') - .next() - .ok_or_else(|| String::from("Output did not contain any newline"))?; - - Ok(token.to_string()) -} - -fn sync_repo(root_path: &Path, repo: &Repo, init_worktree: bool) -> Result<(), String> { - let repo_path = root_path.join(&repo.fullname()); - let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup); - - let mut newly_created = false; - - if repo_path.exists() { - if repo.worktree_setup && !actual_git_directory.exists() { - return Err(String::from( - "Repo already exists, but is not using a worktree setup", - )); - }; - } else if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().is_empty() { - print_repo_action( - &repo.name, - "Repository does not have remotes configured, initializing new", - ); - match RepoHandle::init(&repo_path, repo.worktree_setup) { - Ok(r) => { - print_repo_success(&repo.name, "Repository created"); - Some(r) - } - Err(e) => { - return Err(format!("Repository failed during init: {}", e)); - } - }; - } else { - let first = repo.remotes.as_ref().unwrap().first().unwrap(); - - match clone_repo(first, &repo_path, repo.worktree_setup) { - Ok(_) => { - print_repo_success(&repo.name, "Repository successfully cloned"); - } - Err(e) => { - return Err(format!("Repository failed during clone: {}", e)); - } - }; - - newly_created = true; - } - - let repo_handle = match RepoHandle::open(&repo_path, repo.worktree_setup) { - Ok(repo) => repo, - Err(error) => { - if !repo.worktree_setup && RepoHandle::open(&repo_path, true).is_ok() { - return Err(String::from( - "Repo already exists, but is using a worktree setup", - )); - } else { - return Err(format!("Opening repository failed: {}", error)); - } - } - }; - - if newly_created && repo.worktree_setup && init_worktree { - match repo_handle.default_branch() { - Ok(branch) => { - add_worktree(&repo_path, &branch.name()?, None, None, false)?; - } - Err(_error) => print_repo_error( - &repo.name, - "Could not determine default branch, skipping worktree initializtion", - ), - } - } - if let Some(remotes) = &repo.remotes { - let current_remotes: Vec = repo_handle - .remotes() - .map_err(|error| format!("Repository failed during getting the remotes: {}", error))?; - - for remote in remotes { - let current_remote = repo_handle.find_remote(&remote.name)?; - - match current_remote { - Some(current_remote) => { - let current_url = current_remote.url(); - - if remote.url != current_url { - print_repo_action( - &repo.name, - &format!("Updating remote {} to \"{}\"", &remote.name, &remote.url), - ); - if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) { - return Err(format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e)); - }; - } - } - None => { - print_repo_action( - &repo.name, - &format!( - "Setting up new remote \"{}\" to \"{}\"", - &remote.name, &remote.url - ), - ); - if let Err(e) = repo_handle.new_remote(&remote.name, &remote.url) { - return Err(format!( - "Repository failed during setting the remotes: {}", - e - )); - } - } - } - } - - for current_remote in ¤t_remotes { - if !remotes.iter().any(|r| &r.name == current_remote) { - print_repo_action( - &repo.name, - &format!("Deleting remote \"{}\"", ¤t_remote,), - ); - if let Err(e) = repo_handle.remote_delete(current_remote) { - return Err(format!( - "Repository failed during deleting remote \"{}\": {}", - ¤t_remote, e - )); - } - } - } - } - Ok(()) -} - -pub fn find_unmanaged_repos( - root_path: &Path, - managed_repos: &[Repo], -) -> Result, String> { - let mut unmanaged_repos = Vec::new(); - - for repo_path in find_repo_paths(root_path)? { - if !managed_repos - .iter() - .any(|r| Path::new(root_path).join(r.fullname()) == repo_path) - { - unmanaged_repos.push(repo_path); - } - } - Ok(unmanaged_repos) -} - -pub fn sync_trees(config: Config, init_worktree: bool) -> Result { - let mut failures = false; - - let mut unmanaged_repos_absolute_paths = vec![]; - let mut managed_repos_absolute_paths = vec![]; - - let trees = config.trees()?; - - for tree in trees { - let repos: Vec = tree - .repos - .unwrap_or_default() - .into_iter() - .map(|repo| repo.into_repo()) - .collect(); - - let root_path = expand_path(Path::new(&tree.root)); - - for repo in &repos { - managed_repos_absolute_paths.push(root_path.join(repo.fullname())); - match sync_repo(&root_path, repo, init_worktree) { - Ok(_) => print_repo_success(&repo.name, "OK"), - Err(error) => { - print_repo_error(&repo.name, &error); - failures = true; - } - } - } - - match find_unmanaged_repos(&root_path, &repos) { - Ok(repos) => { - unmanaged_repos_absolute_paths.extend(repos); - } - Err(error) => { - print_error(&format!("Error getting unmanaged repos: {}", error)); - failures = true; - } - } - } - - for unmanaged_repo_absolute_path in &unmanaged_repos_absolute_paths { - if managed_repos_absolute_paths - .iter() - .any(|managed_repo_absolute_path| { - managed_repo_absolute_path == unmanaged_repo_absolute_path - }) - { - continue; - } - print_warning(&format!( - "Found unmanaged repository: \"{}\"", - path_as_string(unmanaged_repo_absolute_path) - )); - } - - Ok(!failures) -} - -/// Finds repositories recursively, returning their path -fn find_repo_paths(path: &Path) -> Result, String> { - let mut repos = Vec::new(); - - let git_dir = path.join(".git"); - let git_worktree = path.join(GIT_MAIN_WORKTREE_DIRECTORY); - - if git_dir.exists() || git_worktree.exists() { - repos.push(path.to_path_buf()); - } else { - match fs::read_dir(path) { - Ok(contents) => { - for content in contents { - match content { - Ok(entry) => { - let path = entry.path(); - if path.is_symlink() { - continue; - } - if path.is_dir() { - match find_repo_paths(&path) { - Ok(ref mut r) => repos.append(r), - Err(error) => return Err(error), - } - } - } - Err(e) => { - return Err(format!("Error accessing directory: {}", e)); - } - }; - } - } - Err(e) => { - return Err(format!( - "Failed to open \"{}\": {}", - &path.display(), - match e.kind() { - std::io::ErrorKind::NotADirectory => - String::from("directory expected, but path is not a directory"), - std::io::ErrorKind::NotFound => String::from("not found"), - _ => format!("{:?}", e.kind()), - } - )); - } - }; - } - - Ok(repos) -} - -fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf { - match is_worktree { - false => path.to_path_buf(), - true => path.join(GIT_MAIN_WORKTREE_DIRECTORY), - } -} - /// Find all git repositories under root, recursively /// /// The bool in the return value specifies whether there is a repository /// in root itself. #[allow(clippy::type_complexity)] -fn find_repos(root: &Path) -> Result, Vec, bool)>, String> { - let mut repos: Vec = Vec::new(); +fn find_repos(root: &Path) -> Result, Vec, bool)>, String> { + let mut repos: Vec = Vec::new(); let mut repo_in_root = false; let mut warnings = Vec::new(); - for path in find_repo_paths(root)? { - let is_worktree = RepoHandle::detect_worktree(&path); + for path in tree::find_repo_paths(root)? { + let is_worktree = repo::RepoHandle::detect_worktree(&path); if path == root { repo_in_root = true; } - match RepoHandle::open(&path, is_worktree) { + match repo::RepoHandle::open(&path, is_worktree) { Err(error) => { warnings.push(format!( "Error opening repo {}{}: {}", @@ -436,32 +50,32 @@ fn find_repos(root: &Path) -> Result, Vec, bool)>, Str Err(error) => { warnings.push(format!( "{}: Error getting remotes: {}", - &path_as_string(&path), + &path::path_as_string(&path), error )); continue; } }; - let mut results: Vec = Vec::new(); + let mut results: Vec = Vec::new(); for remote_name in remotes.iter() { match repo.find_remote(remote_name)? { Some(remote) => { let name = remote.name(); let url = remote.url(); - let remote_type = match detect_remote_type(&url) { + let remote_type = match repo::detect_remote_type(&url) { Some(t) => t, None => { warnings.push(format!( "{}: Could not detect remote type of \"{}\"", - &path_as_string(&path), + &path::path_as_string(&path), &url )); continue; } }; - results.push(Remote { + results.push(repo::Remote { name, url, remote_type, @@ -470,7 +84,7 @@ fn find_repos(root: &Path) -> Result, Vec, bool)>, Str None => { warnings.push(format!( "{}: Remote {} not found", - &path_as_string(&path), + &path::path_as_string(&path), remote_name )); continue; @@ -483,7 +97,9 @@ fn find_repos(root: &Path) -> Result, Vec, bool)>, Str ( None, match &root.parent() { - Some(parent) => path_as_string(path.strip_prefix(parent).unwrap()), + Some(parent) => { + path::path_as_string(path.strip_prefix(parent).unwrap()) + } None => { warnings.push(String::from("Getting name of the search root failed. Do you have a git repository in \"/\"?")); continue; @@ -495,15 +111,15 @@ fn find_repos(root: &Path) -> Result, Vec, bool)>, Str let namespace = name.parent().unwrap(); ( if namespace != Path::new("") { - Some(path_as_string(namespace).to_string()) + Some(path::path_as_string(namespace).to_string()) } else { None }, - path_as_string(name), + path::path_as_string(name), ) }; - repos.push(Repo { + repos.push(repo::Repo { name, namespace, remotes: Some(remotes), @@ -515,10 +131,10 @@ fn find_repos(root: &Path) -> Result, Vec, bool)>, Str Ok(Some((repos, warnings, repo_in_root))) } -pub fn find_in_tree(path: &Path) -> Result<(Tree, Vec), String> { +pub fn find_in_tree(path: &Path) -> Result<(tree::Tree, Vec), String> { let mut warnings = Vec::new(); - let (repos, repo_in_root): (Vec, bool) = match find_repos(path)? { + let (repos, repo_in_root): (Vec, bool) = match find_repos(path)? { Some((vec, mut repo_warnings, repo_in_root)) => { warnings.append(&mut repo_warnings); (vec, repo_in_root) @@ -539,171 +155,10 @@ pub fn find_in_tree(path: &Path) -> Result<(Tree, Vec), String> { } Ok(( - Tree { + tree::Tree { root: root.into_os_string().into_string().unwrap(), repos, }, warnings, )) } - -pub fn add_worktree( - directory: &Path, - name: &str, - subdirectory: Option<&Path>, - track: Option<(&str, &str)>, - no_track: bool, -) -> Result<(), String> { - let repo = RepoHandle::open(directory, true).map_err(|error| match error.kind { - RepoErrorKind::NotFound => { - String::from("Current directory does not contain a worktree setup") - } - _ => format!("Error opening repo: {}", error), - })?; - - let config = repo::read_worktree_root_config(directory)?; - - if repo.find_worktree(name).is_ok() { - return Err(format!("Worktree {} already exists", &name)); - } - - let path = match subdirectory { - Some(dir) => directory.join(dir).join(name), - None => directory.join(Path::new(name)), - }; - - let mut remote_branch_exists = false; - - let default_checkout = || repo.default_branch()?.to_commit(); - - let checkout_commit; - if no_track { - checkout_commit = default_checkout()?; - } else { - match track { - Some((remote_name, remote_branch_name)) => { - let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); - match remote_branch { - Ok(branch) => { - remote_branch_exists = true; - checkout_commit = branch.to_commit()?; - } - Err(_) => { - remote_branch_exists = false; - checkout_commit = default_checkout()?; - } - } - } - None => match &config { - None => checkout_commit = default_checkout()?, - Some(config) => match &config.track { - None => checkout_commit = default_checkout()?, - Some(track_config) => { - if track_config.default { - let remote_branch = - repo.find_remote_branch(&track_config.default_remote, name); - match remote_branch { - Ok(branch) => { - remote_branch_exists = true; - checkout_commit = branch.to_commit()?; - } - Err(_) => { - checkout_commit = default_checkout()?; - } - } - } else { - checkout_commit = default_checkout()?; - } - } - }, - }, - }; - } - - let mut target_branch = match repo.find_local_branch(name) { - Ok(branchref) => branchref, - Err(_) => repo.create_branch(name, &checkout_commit)?, - }; - - fn push( - remote: &mut repo::RemoteHandle, - branch_name: &str, - remote_branch_name: &str, - repo: &repo::RepoHandle, - ) -> Result<(), String> { - if !remote.is_pushable()? { - return Err(format!( - "Cannot push to non-pushable remote {}", - remote.url() - )); - } - remote.push(branch_name, remote_branch_name, repo) - } - - if !no_track { - if let Some((remote_name, remote_branch_name)) = track { - if remote_branch_exists { - target_branch.set_upstream(remote_name, remote_branch_name)?; - } else { - let mut remote = repo - .find_remote(remote_name) - .map_err(|error| format!("Error getting remote {}: {}", remote_name, error))? - .ok_or_else(|| format!("Remote {} not found", remote_name))?; - - push( - &mut remote, - &target_branch.name()?, - remote_branch_name, - &repo, - )?; - - target_branch.set_upstream(remote_name, remote_branch_name)?; - } - } else if let Some(config) = config { - if let Some(track_config) = config.track { - if track_config.default { - let remote_name = track_config.default_remote; - if remote_branch_exists { - target_branch.set_upstream(&remote_name, name)?; - } else { - let remote_branch_name = match track_config.default_remote_prefix { - Some(prefix) => { - format!("{}{}{}", &prefix, BRANCH_NAMESPACE_SEPARATOR, &name) - } - None => name.to_string(), - }; - - let mut remote = repo - .find_remote(&remote_name) - .map_err(|error| { - format!("Error getting remote {}: {}", remote_name, error) - })? - .ok_or_else(|| format!("Remote {} not found", remote_name))?; - - if !remote.is_pushable()? { - return Err(format!( - "Cannot push to non-pushable remote {}", - remote.url() - )); - } - push( - &mut remote, - &target_branch.name()?, - &remote_branch_name, - &repo, - )?; - - target_branch.set_upstream(&remote_name, &remote_branch_name)?; - } - } - } - } - } - - if let Some(subdirectory) = subdirectory { - std::fs::create_dir_all(subdirectory).map_err(|error| error.to_string())?; - } - repo.new_worktree(name, &path, &target_branch)?; - - Ok(()) -} diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..897d340 --- /dev/null +++ b/src/path.rs @@ -0,0 +1,84 @@ +use std::path::{Path, PathBuf}; +use std::process; + +use super::output::*; + +#[cfg(test)] +mod tests { + use super::*; + + fn setup() { + std::env::set_var("HOME", "/home/test"); + } + + #[test] + fn check_expand_tilde() { + setup(); + assert_eq!( + expand_path(Path::new("~/file")), + Path::new("/home/test/file") + ); + } + + #[test] + fn check_expand_invalid_tilde() { + setup(); + assert_eq!( + expand_path(Path::new("/home/~/file")), + Path::new("/home/~/file") + ); + } + + #[test] + fn check_expand_home() { + setup(); + assert_eq!( + expand_path(Path::new("$HOME/file")), + Path::new("/home/test/file") + ); + assert_eq!( + expand_path(Path::new("${HOME}/file")), + Path::new("/home/test/file") + ); + } +} + +pub fn path_as_string(path: &Path) -> String { + path.to_path_buf().into_os_string().into_string().unwrap() +} + +pub fn env_home() -> PathBuf { + match std::env::var("HOME") { + Ok(path) => Path::new(&path).to_path_buf(), + Err(e) => { + print_error(&format!("Unable to read HOME: {}", e)); + process::exit(1); + } + } +} + +pub fn expand_path(path: &Path) -> PathBuf { + fn home_dir() -> Option { + Some(env_home()) + } + + let expanded_path = match shellexpand::full_with_context( + &path_as_string(path), + home_dir, + |name| -> Result, &'static str> { + match name { + "HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))), + _ => Ok(None), + } + }, + ) { + Ok(std::borrow::Cow::Borrowed(path)) => path.to_owned(), + Ok(std::borrow::Cow::Owned(path)) => path, + Err(e) => { + print_error(&format!("Unable to expand root: {}", e)); + process::exit(1); + } + }; + + Path::new(&expanded_path).to_path_buf() +} diff --git a/src/provider/mod.rs b/src/provider/mod.rs index 20253b6..1e59bd4 100644 --- a/src/provider/mod.rs +++ b/src/provider/mod.rs @@ -9,7 +9,7 @@ pub mod gitlab; pub use github::Github; pub use gitlab::Gitlab; -use crate::{Remote, RemoteType, Repo}; +use super::repo; use std::collections::HashMap; @@ -29,15 +29,20 @@ enum ProjectResponse { } pub trait Project { - fn into_repo_config(self, provider_name: &str, worktree_setup: bool, force_ssh: bool) -> Repo + fn into_repo_config( + self, + provider_name: &str, + worktree_setup: bool, + force_ssh: bool, + ) -> repo::Repo where Self: Sized, { - Repo { + repo::Repo { name: self.name(), namespace: self.namespace(), worktree_setup, - remotes: Some(vec![Remote { + remotes: Some(vec![repo::Remote { name: String::from(provider_name), url: if force_ssh || self.private() { self.ssh_url() @@ -45,9 +50,9 @@ pub trait Project { self.http_url() }, remote_type: if force_ssh || self.private() { - RemoteType::Ssh + repo::RemoteType::Ssh } else { - RemoteType::Https + repo::RemoteType::Https }, }]), } @@ -201,7 +206,7 @@ pub trait Provider { &self, worktree_setup: bool, force_ssh: bool, - ) -> Result, Vec>, String> { + ) -> Result, Vec>, String> { let mut repos = vec![]; if self.filter().owner { @@ -278,7 +283,7 @@ pub trait Provider { } } - let mut ret: HashMap, Vec> = HashMap::new(); + let mut ret: HashMap, Vec> = HashMap::new(); for repo in repos { let namespace = repo.namespace(); diff --git a/src/repo.rs b/src/repo.rs index e8782f6..48b1a65 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -3,9 +3,13 @@ use std::path::Path; use git2::Repository; -use crate::output::*; +use super::output::*; +use super::path; +use super::worktree; const WORKTREE_CONFIG_FILE_NAME: &str = "grm.toml"; +const GIT_CONFIG_BARE_KEY: &str = "core.bare"; +const GIT_CONFIG_PUSH_DEFAULT: &str = "push.default"; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -506,7 +510,7 @@ impl RepoHandle { false => Repository::open, }; let path = match is_worktree { - true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY), + true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY), false => path.to_path_buf(), }; match open_func(path) { @@ -679,7 +683,7 @@ impl RepoHandle { pub fn init(path: &Path, is_worktree: bool) -> Result { let repo = match is_worktree { false => Repository::init(path).map_err(convert_libgit2_error)?, - true => Repository::init_bare(path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY)) + true => Repository::init_bare(path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY)) .map_err(convert_libgit2_error)?, }; @@ -742,8 +746,8 @@ impl RepoHandle { let mut config = self.config()?; config - .set_bool(crate::GIT_CONFIG_BARE_KEY, value) - .map_err(|error| format!("Could not set {}: {}", crate::GIT_CONFIG_BARE_KEY, error)) + .set_bool(GIT_CONFIG_BARE_KEY, value) + .map_err(|error| format!("Could not set {}: {}", GIT_CONFIG_BARE_KEY, error)) } pub fn convert_to_worktree( @@ -766,7 +770,7 @@ impl RepoHandle { return Err(WorktreeConversionFailureReason::Ignored); } - std::fs::rename(".git", crate::GIT_MAIN_WORKTREE_DIRECTORY).map_err(|error| { + std::fs::rename(".git", worktree::GIT_MAIN_WORKTREE_DIRECTORY).map_err(|error| { WorktreeConversionFailureReason::Error(format!( "Error moving .git directory: {}", error @@ -786,7 +790,7 @@ impl RepoHandle { Ok(entry) => { let path = entry.path(); // unwrap is safe here, the path will ALWAYS have a file component - if path.file_name().unwrap() == crate::GIT_MAIN_WORKTREE_DIRECTORY { + if path.file_name().unwrap() == worktree::GIT_MAIN_WORKTREE_DIRECTORY { continue; } if path.is_file() || path.is_symlink() { @@ -835,18 +839,12 @@ impl RepoHandle { config .set_str( - crate::GIT_CONFIG_PUSH_DEFAULT, + GIT_CONFIG_PUSH_DEFAULT, match value { GitPushDefaultSetting::Upstream => "upstream", }, ) - .map_err(|error| { - format!( - "Could not set {}: {}", - crate::GIT_CONFIG_PUSH_DEFAULT, - error - ) - }) + .map_err(|error| format!("Could not set {}: {}", GIT_CONFIG_PUSH_DEFAULT, error)) } pub fn has_untracked_files(&self, is_worktree: bool) -> Result { @@ -1105,7 +1103,7 @@ impl RepoHandle { })?; if branch_name != name - && !branch_name.ends_with(&format!("{}{}", crate::BRANCH_NAMESPACE_SEPARATOR, name)) + && !branch_name.ends_with(&format!("{}{}", super::BRANCH_NAMESPACE_SEPARATOR, name)) { return Err(WorktreeRemoveFailureReason::Error(format!( "Branch {} is checked out in worktree, this does not look correct", @@ -1275,7 +1273,7 @@ impl RepoHandle { let mut unmanaged_worktrees = Vec::new(); for entry in std::fs::read_dir(&directory).map_err(|error| error.to_string())? { - let dirname = crate::path_as_string( + let dirname = path::path_as_string( entry .map_err(|error| error.to_string())? .path() @@ -1308,7 +1306,7 @@ impl RepoHandle { }, }; - if dirname == crate::GIT_MAIN_WORKTREE_DIRECTORY { + if dirname == worktree::GIT_MAIN_WORKTREE_DIRECTORY { continue; } if dirname == WORKTREE_CONFIG_FILE_NAME { @@ -1327,7 +1325,7 @@ impl RepoHandle { } pub fn detect_worktree(path: &Path) -> bool { - path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY).exists() + path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY).exists() } } @@ -1486,7 +1484,7 @@ pub fn clone_repo( ) -> Result<(), Box> { let clone_target = match is_worktree { false => path.to_path_buf(), - true => path.join(crate::GIT_MAIN_WORKTREE_DIRECTORY), + true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY), }; print_action(&format!( diff --git a/src/table.rs b/src/table.rs index 72b0f78..5256a6a 100644 --- a/src/table.rs +++ b/src/table.rs @@ -1,4 +1,6 @@ -use crate::RepoHandle; +use super::config; +use super::path; +use super::repo; use comfy_table::{Cell, Table}; @@ -21,7 +23,7 @@ fn add_table_header(table: &mut Table) { fn add_repo_status( table: &mut Table, repo_name: &str, - repo_handle: &crate::RepoHandle, + repo_handle: &repo::RepoHandle, is_worktree: bool, ) -> Result<(), String> { let repo_status = repo_handle.status(is_worktree)?; @@ -65,11 +67,11 @@ fn add_repo_status( " <{}>{}", remote_branch_name, &match remote_tracking_status { - crate::RemoteTrackingStatus::UpToDate => + repo::RemoteTrackingStatus::UpToDate => String::from(" \u{2714}"), - crate::RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d), - crate::RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d), - crate::RemoteTrackingStatus::Diverged(d1, d2) => + repo::RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d), + repo::RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d), + repo::RemoteTrackingStatus::Diverged(d1, d2) => format!(" [+{}/-{}]", &d1, &d2), } ) @@ -99,7 +101,7 @@ fn add_repo_status( // Don't return table, return a type that implements Display(?) pub fn get_worktree_status_table( - repo: &crate::RepoHandle, + repo: &repo::RepoHandle, directory: &Path, ) -> Result<(impl std::fmt::Display, Vec), String> { let worktrees = repo.get_worktrees()?; @@ -111,7 +113,7 @@ pub fn get_worktree_status_table( for worktree in &worktrees { let worktree_dir = &directory.join(&worktree.name()); if worktree_dir.exists() { - let repo = match crate::RepoHandle::open(worktree_dir, false) { + let repo = match repo::RepoHandle::open(worktree_dir, false) { Ok(repo) => repo, Err(error) => { errors.push(format!( @@ -132,7 +134,7 @@ pub fn get_worktree_status_table( )); } } - for worktree in RepoHandle::find_unmanaged_worktrees(repo, directory)? { + for worktree in repo::RepoHandle::find_unmanaged_worktrees(repo, directory)? { errors.push(format!( "Found {}, which is not a valid worktree directory!", &worktree @@ -141,13 +143,13 @@ pub fn get_worktree_status_table( Ok((table, errors)) } -pub fn get_status_table(config: crate::Config) -> Result<(Vec, Vec), String> { +pub fn get_status_table(config: config::Config) -> Result<(Vec
, Vec), String> { let mut errors = Vec::new(); let mut tables = Vec::new(); for tree in config.trees()? { let repos = tree.repos.unwrap_or_default(); - let root_path = crate::expand_path(Path::new(&tree.root)); + let root_path = path::expand_path(Path::new(&tree.root)); let mut table = Table::new(); add_table_header(&mut table); @@ -163,12 +165,12 @@ pub fn get_status_table(config: crate::Config) -> Result<(Vec
, Vec repo, Err(error) => { - if error.kind == crate::RepoErrorKind::NotFound { + if error.kind == repo::RepoErrorKind::NotFound { errors.push(format!( "{}: No git repository found. Run sync?", &repo.name @@ -206,8 +208,8 @@ fn add_worktree_table_header(table: &mut Table) { fn add_worktree_status( table: &mut Table, - worktree: &crate::repo::Worktree, - repo: &crate::RepoHandle, + worktree: &repo::Worktree, + repo: &repo::RepoHandle, ) -> Result<(), String> { let repo_status = repo.status(false)?; @@ -272,13 +274,13 @@ pub fn show_single_repo_status( let mut table = Table::new(); let mut warnings = Vec::new(); - let is_worktree = crate::RepoHandle::detect_worktree(path); + let is_worktree = repo::RepoHandle::detect_worktree(path); add_table_header(&mut table); - let repo_handle = crate::RepoHandle::open(path, is_worktree); + let repo_handle = repo::RepoHandle::open(path, is_worktree); if let Err(error) = repo_handle { - if error.kind == crate::RepoErrorKind::NotFound { + if error.kind == repo::RepoErrorKind::NotFound { return Err(String::from("Directory is not a git directory")); } else { return Err(format!("Opening repository failed: {}", error)); diff --git a/src/tree.rs b/src/tree.rs new file mode 100644 index 0000000..bf35179 --- /dev/null +++ b/src/tree.rs @@ -0,0 +1,268 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use super::config; +use super::output::*; +use super::path; +use super::repo; +use super::worktree; + +pub struct Tree { + pub root: String, + pub repos: Vec, +} + +pub fn find_unmanaged_repos( + root_path: &Path, + managed_repos: &[repo::Repo], +) -> Result, String> { + let mut unmanaged_repos = Vec::new(); + + for repo_path in find_repo_paths(root_path)? { + if !managed_repos + .iter() + .any(|r| Path::new(root_path).join(r.fullname()) == repo_path) + { + unmanaged_repos.push(repo_path); + } + } + Ok(unmanaged_repos) +} + +pub fn sync_trees(config: config::Config, init_worktree: bool) -> Result { + let mut failures = false; + + let mut unmanaged_repos_absolute_paths = vec![]; + let mut managed_repos_absolute_paths = vec![]; + + let trees = config.trees()?; + + for tree in trees { + let repos: Vec = tree + .repos + .unwrap_or_default() + .into_iter() + .map(|repo| repo.into_repo()) + .collect(); + + let root_path = path::expand_path(Path::new(&tree.root)); + + for repo in &repos { + managed_repos_absolute_paths.push(root_path.join(repo.fullname())); + match sync_repo(&root_path, repo, init_worktree) { + Ok(_) => print_repo_success(&repo.name, "OK"), + Err(error) => { + print_repo_error(&repo.name, &error); + failures = true; + } + } + } + + match find_unmanaged_repos(&root_path, &repos) { + Ok(repos) => { + unmanaged_repos_absolute_paths.extend(repos); + } + Err(error) => { + print_error(&format!("Error getting unmanaged repos: {}", error)); + failures = true; + } + } + } + + for unmanaged_repo_absolute_path in &unmanaged_repos_absolute_paths { + if managed_repos_absolute_paths + .iter() + .any(|managed_repo_absolute_path| { + managed_repo_absolute_path == unmanaged_repo_absolute_path + }) + { + continue; + } + print_warning(&format!( + "Found unmanaged repository: \"{}\"", + path::path_as_string(unmanaged_repo_absolute_path) + )); + } + + Ok(!failures) +} + +/// Finds repositories recursively, returning their path +pub fn find_repo_paths(path: &Path) -> Result, String> { + let mut repos = Vec::new(); + + let git_dir = path.join(".git"); + let git_worktree = path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY); + + if git_dir.exists() || git_worktree.exists() { + repos.push(path.to_path_buf()); + } else { + match fs::read_dir(path) { + Ok(contents) => { + for content in contents { + match content { + Ok(entry) => { + let path = entry.path(); + if path.is_symlink() { + continue; + } + if path.is_dir() { + match find_repo_paths(&path) { + Ok(ref mut r) => repos.append(r), + Err(error) => return Err(error), + } + } + } + Err(e) => { + return Err(format!("Error accessing directory: {}", e)); + } + }; + } + } + Err(e) => { + return Err(format!( + "Failed to open \"{}\": {}", + &path.display(), + match e.kind() { + std::io::ErrorKind::NotADirectory => + String::from("directory expected, but path is not a directory"), + std::io::ErrorKind::NotFound => String::from("not found"), + _ => format!("{:?}", e.kind()), + } + )); + } + }; + } + + Ok(repos) +} + +fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result<(), String> { + let repo_path = root_path.join(&repo.fullname()); + let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup); + + let mut newly_created = false; + + if repo_path.exists() { + if repo.worktree_setup && !actual_git_directory.exists() { + return Err(String::from( + "Repo already exists, but is not using a worktree setup", + )); + }; + } else if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().is_empty() { + print_repo_action( + &repo.name, + "Repository does not have remotes configured, initializing new", + ); + match repo::RepoHandle::init(&repo_path, repo.worktree_setup) { + Ok(r) => { + print_repo_success(&repo.name, "Repository created"); + Some(r) + } + Err(e) => { + return Err(format!("Repository failed during init: {}", e)); + } + }; + } else { + let first = repo.remotes.as_ref().unwrap().first().unwrap(); + + match repo::clone_repo(first, &repo_path, repo.worktree_setup) { + Ok(_) => { + print_repo_success(&repo.name, "Repository successfully cloned"); + } + Err(e) => { + return Err(format!("Repository failed during clone: {}", e)); + } + }; + + newly_created = true; + } + + let repo_handle = match repo::RepoHandle::open(&repo_path, repo.worktree_setup) { + Ok(repo) => repo, + Err(error) => { + if !repo.worktree_setup && repo::RepoHandle::open(&repo_path, true).is_ok() { + return Err(String::from( + "Repo already exists, but is using a worktree setup", + )); + } else { + return Err(format!("Opening repository failed: {}", error)); + } + } + }; + + if newly_created && repo.worktree_setup && init_worktree { + match repo_handle.default_branch() { + Ok(branch) => { + worktree::add_worktree(&repo_path, &branch.name()?, None, None, false)?; + } + Err(_error) => print_repo_error( + &repo.name, + "Could not determine default branch, skipping worktree initializtion", + ), + } + } + if let Some(remotes) = &repo.remotes { + let current_remotes: Vec = repo_handle + .remotes() + .map_err(|error| format!("Repository failed during getting the remotes: {}", error))?; + + for remote in remotes { + let current_remote = repo_handle.find_remote(&remote.name)?; + + match current_remote { + Some(current_remote) => { + let current_url = current_remote.url(); + + if remote.url != current_url { + print_repo_action( + &repo.name, + &format!("Updating remote {} to \"{}\"", &remote.name, &remote.url), + ); + if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) { + return Err(format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e)); + }; + } + } + None => { + print_repo_action( + &repo.name, + &format!( + "Setting up new remote \"{}\" to \"{}\"", + &remote.name, &remote.url + ), + ); + if let Err(e) = repo_handle.new_remote(&remote.name, &remote.url) { + return Err(format!( + "Repository failed during setting the remotes: {}", + e + )); + } + } + } + } + + for current_remote in ¤t_remotes { + if !remotes.iter().any(|r| &r.name == current_remote) { + print_repo_action( + &repo.name, + &format!("Deleting remote \"{}\"", ¤t_remote,), + ); + if let Err(e) = repo_handle.remote_delete(current_remote) { + return Err(format!( + "Repository failed during deleting remote \"{}\": {}", + ¤t_remote, e + )); + } + } + } + } + Ok(()) +} + +fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf { + match is_worktree { + false => path.to_path_buf(), + true => path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY), + } +} diff --git a/src/worktree.rs b/src/worktree.rs new file mode 100644 index 0000000..1f85499 --- /dev/null +++ b/src/worktree.rs @@ -0,0 +1,166 @@ +use std::path::Path; + +use super::repo; + +pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree"; + +pub fn add_worktree( + directory: &Path, + name: &str, + subdirectory: Option<&Path>, + track: Option<(&str, &str)>, + no_track: bool, +) -> Result<(), String> { + let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error.kind { + repo::RepoErrorKind::NotFound => { + String::from("Current directory does not contain a worktree setup") + } + _ => format!("Error opening repo: {}", error), + })?; + + let config = repo::read_worktree_root_config(directory)?; + + if repo.find_worktree(name).is_ok() { + return Err(format!("Worktree {} already exists", &name)); + } + + let path = match subdirectory { + Some(dir) => directory.join(dir).join(name), + None => directory.join(Path::new(name)), + }; + + let mut remote_branch_exists = false; + + let default_checkout = || repo.default_branch()?.to_commit(); + + let checkout_commit; + if no_track { + checkout_commit = default_checkout()?; + } else { + match track { + Some((remote_name, remote_branch_name)) => { + let remote_branch = repo.find_remote_branch(remote_name, remote_branch_name); + match remote_branch { + Ok(branch) => { + remote_branch_exists = true; + checkout_commit = branch.to_commit()?; + } + Err(_) => { + remote_branch_exists = false; + checkout_commit = default_checkout()?; + } + } + } + None => match &config { + None => checkout_commit = default_checkout()?, + Some(config) => match &config.track { + None => checkout_commit = default_checkout()?, + Some(track_config) => { + if track_config.default { + let remote_branch = + repo.find_remote_branch(&track_config.default_remote, name); + match remote_branch { + Ok(branch) => { + remote_branch_exists = true; + checkout_commit = branch.to_commit()?; + } + Err(_) => { + checkout_commit = default_checkout()?; + } + } + } else { + checkout_commit = default_checkout()?; + } + } + }, + }, + }; + } + + let mut target_branch = match repo.find_local_branch(name) { + Ok(branchref) => branchref, + Err(_) => repo.create_branch(name, &checkout_commit)?, + }; + + fn push( + remote: &mut repo::RemoteHandle, + branch_name: &str, + remote_branch_name: &str, + repo: &repo::RepoHandle, + ) -> Result<(), String> { + if !remote.is_pushable()? { + return Err(format!( + "Cannot push to non-pushable remote {}", + remote.url() + )); + } + remote.push(branch_name, remote_branch_name, repo) + } + + if !no_track { + if let Some((remote_name, remote_branch_name)) = track { + if remote_branch_exists { + target_branch.set_upstream(remote_name, remote_branch_name)?; + } else { + let mut remote = repo + .find_remote(remote_name) + .map_err(|error| format!("Error getting remote {}: {}", remote_name, error))? + .ok_or_else(|| format!("Remote {} not found", remote_name))?; + + push( + &mut remote, + &target_branch.name()?, + remote_branch_name, + &repo, + )?; + + target_branch.set_upstream(remote_name, remote_branch_name)?; + } + } else if let Some(config) = config { + if let Some(track_config) = config.track { + if track_config.default { + let remote_name = track_config.default_remote; + if remote_branch_exists { + target_branch.set_upstream(&remote_name, name)?; + } else { + let remote_branch_name = match track_config.default_remote_prefix { + Some(prefix) => { + format!("{}{}{}", &prefix, super::BRANCH_NAMESPACE_SEPARATOR, &name) + } + None => name.to_string(), + }; + + let mut remote = repo + .find_remote(&remote_name) + .map_err(|error| { + format!("Error getting remote {}: {}", remote_name, error) + })? + .ok_or_else(|| format!("Remote {} not found", remote_name))?; + + if !remote.is_pushable()? { + return Err(format!( + "Cannot push to non-pushable remote {}", + remote.url() + )); + } + push( + &mut remote, + &target_branch.name()?, + &remote_branch_name, + &repo, + )?; + + target_branch.set_upstream(&remote_name, &remote_branch_name)?; + } + } + } + } + } + + if let Some(subdirectory) = subdirectory { + std::fs::create_dir_all(subdirectory).map_err(|error| error.to_string())?; + } + repo.new_worktree(name, &path, &target_branch)?; + + Ok(()) +}