Restructure into smaller modules

This commit is contained in:
2022-05-27 23:37:54 +02:00
parent 5d7480f7a4
commit 32eb4676ee
12 changed files with 720 additions and 707 deletions

36
src/auth.rs Normal file
View File

@@ -0,0 +1,36 @@
use std::process;
pub fn get_token_from_command(command: &str) -> Result<String, String> {
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())
}

View File

@@ -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<Tree>) -> Self {
pub fn from_trees(vec: Vec<tree::Tree>) -> 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<Repo>) -> Self {
pub fn from_repos(root: String, repos: Vec<repo::Repo>) -> 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()),

View File

@@ -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()]

View File

@@ -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));

View File

@@ -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<Repo>,
}
#[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<PathBuf> {
Some(env_home())
}
let expanded_path = match shellexpand::full_with_context(
&path_as_string(path),
home_dir,
|name| -> Result<Option<String>, &'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<String, String> {
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<String> = 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 &current_remotes {
if !remotes.iter().any(|r| &r.name == current_remote) {
print_repo_action(
&repo.name,
&format!("Deleting remote \"{}\"", &current_remote,),
);
if let Err(e) = repo_handle.remote_delete(current_remote) {
return Err(format!(
"Repository failed during deleting remote \"{}\": {}",
&current_remote, e
));
}
}
}
}
Ok(())
}
pub fn find_unmanaged_repos(
root_path: &Path,
managed_repos: &[Repo],
) -> Result<Vec<PathBuf>, 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<bool, String> {
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<Repo> = 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<Vec<PathBuf>, 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<Option<(Vec<Repo>, Vec<String>, bool)>, String> {
let mut repos: Vec<Repo> = Vec::new();
fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)>, String> {
let mut repos: Vec<repo::Repo> = 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<Option<(Vec<Repo>, Vec<String>, 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<Remote> = Vec::new();
let mut results: Vec<repo::Remote> = 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<Option<(Vec<Repo>, Vec<String>, 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<Option<(Vec<Repo>, Vec<String>, 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<Option<(Vec<Repo>, Vec<String>, 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<Option<(Vec<Repo>, Vec<String>, bool)>, Str
Ok(Some((repos, warnings, repo_in_root)))
}
pub fn find_in_tree(path: &Path) -> Result<(Tree, Vec<String>), String> {
pub fn find_in_tree(path: &Path) -> Result<(tree::Tree, Vec<String>), String> {
let mut warnings = Vec::new();
let (repos, repo_in_root): (Vec<Repo>, bool) = match find_repos(path)? {
let (repos, repo_in_root): (Vec<repo::Repo>, 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>), 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(())
}

84
src/path.rs Normal file
View File

@@ -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<PathBuf> {
Some(env_home())
}
let expanded_path = match shellexpand::full_with_context(
&path_as_string(path),
home_dir,
|name| -> Result<Option<String>, &'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()
}

View File

@@ -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<T, U> {
}
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<HashMap<Option<String>, Vec<Repo>>, String> {
) -> Result<HashMap<Option<String>, Vec<repo::Repo>>, String> {
let mut repos = vec![];
if self.filter().owner {
@@ -278,7 +283,7 @@ pub trait Provider {
}
}
let mut ret: HashMap<Option<String>, Vec<Repo>> = HashMap::new();
let mut ret: HashMap<Option<String>, Vec<repo::Repo>> = HashMap::new();
for repo in repos {
let namespace = repo.namespace();

View File

@@ -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<Self, String> {
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<bool, String> {
@@ -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<dyn std::error::Error>> {
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!(

View File

@@ -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>), 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<Table>, Vec<String>), String> {
pub fn get_status_table(config: config::Config) -> Result<(Vec<Table>, Vec<String>), 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<Table>, Vec<String
continue;
}
let repo_handle = crate::RepoHandle::open(&repo_path, repo.worktree_setup);
let repo_handle = repo::RepoHandle::open(&repo_path, repo.worktree_setup);
let repo_handle = match repo_handle {
Ok(repo) => 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));

268
src/tree.rs Normal file
View File

@@ -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<repo::Repo>,
}
pub fn find_unmanaged_repos(
root_path: &Path,
managed_repos: &[repo::Repo],
) -> Result<Vec<PathBuf>, 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<bool, String> {
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<repo::Repo> = 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<Vec<PathBuf>, 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<String> = 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 &current_remotes {
if !remotes.iter().any(|r| &r.name == current_remote) {
print_repo_action(
&repo.name,
&format!("Deleting remote \"{}\"", &current_remote,),
);
if let Err(e) = repo_handle.remote_delete(current_remote) {
return Err(format!(
"Repository failed during deleting remote \"{}\": {}",
&current_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),
}
}

166
src/worktree.rs Normal file
View File

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