Restructure into smaller modules
This commit is contained in:
0
BRANCH_NAMESPACE_SEPARATOR
Normal file
0
BRANCH_NAMESPACE_SEPARATOR
Normal file
36
src/auth.rs
Normal file
36
src/auth.rs
Normal 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())
|
||||
}
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()]
|
||||
|
||||
118
src/grm/main.rs
118
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,8 +238,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
cmd::FindAction::Config(args) => {
|
||||
let config: crate::config::ConfigProvider =
|
||||
match config::read_config(&args.config) {
|
||||
let config: config::ConfigProvider = match config::read_config(&args.config) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
print_error(&error);
|
||||
@@ -246,7 +246,7 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
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,12 +720,8 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
let config =
|
||||
grm::repo::read_worktree_root_config(&cwd).unwrap_or_else(|error| {
|
||||
print_error(&format!(
|
||||
"Failed to read worktree configuration: {}",
|
||||
error
|
||||
));
|
||||
let config = repo::read_worktree_root_config(&cwd).unwrap_or_else(|error| {
|
||||
print_error(&format!("Failed to read worktree configuration: {}", error));
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
|
||||
595
src/lib.rs
595
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<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 ¤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<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
84
src/path.rs
Normal 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()
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
38
src/repo.rs
38
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<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!(
|
||||
|
||||
38
src/table.rs
38
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>), 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
268
src/tree.rs
Normal 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 ¤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),
|
||||
}
|
||||
}
|
||||
166
src/worktree.rs
Normal file
166
src/worktree.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user