|
|
|
use crossterm::style::Stylize;
|
|
|
|
use semver::Version;
|
|
|
|
use std::{
|
|
|
|
collections::HashMap,
|
|
|
|
fs,
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
};
|
|
|
|
|
|
|
|
use toml::{
|
|
|
|
value::{Map, Table},
|
|
|
|
Value,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub mod cli;
|
|
|
|
mod colors;
|
|
|
|
mod error;
|
|
|
|
mod project;
|
|
|
|
mod systems;
|
|
|
|
|
|
|
|
use colors::*;
|
|
|
|
|
|
|
|
pub use error::*;
|
|
|
|
pub use project::*;
|
|
|
|
|
|
|
|
pub struct Vcs {
|
|
|
|
work_dir: PathBuf,
|
|
|
|
profiles: Vec<String>,
|
|
|
|
projects: Vec<Project>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Vcs {
|
|
|
|
pub fn new(work_dir: PathBuf) -> Result<Self, Error> {
|
|
|
|
// find Cargo.toml recursively from given path
|
|
|
|
let toml_path = Self::search(&work_dir)?;
|
|
|
|
// set work_dir to Cargo.toml parent folder
|
|
|
|
let work_dir = toml_path
|
|
|
|
.parent()
|
|
|
|
.ok_or_else(|| {
|
|
|
|
Error::input_error(
|
|
|
|
"Invalid work_dir found",
|
|
|
|
toml_path.to_str().expect("Invalid UTF-8 in path"),
|
|
|
|
)
|
|
|
|
})?
|
|
|
|
.to_path_buf();
|
|
|
|
|
|
|
|
if !toml_path.exists() {
|
|
|
|
return Err(Error::input_error(
|
|
|
|
"work_dir invalid",
|
|
|
|
&work_dir.display().to_string(),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
if !toml_path.exists() {
|
|
|
|
return Err(Error::input_error(
|
|
|
|
"Unable to find Cargo.toml",
|
|
|
|
&toml_path.display().to_string(),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
let workspace = Self::get_workspace_toml(&toml_path)?;
|
|
|
|
|
|
|
|
let members = workspace
|
|
|
|
.get("members")
|
|
|
|
.ok_or_else(|| Error::cargo_error("Cargo.toml missing [members] array"))?
|
|
|
|
.as_array()
|
|
|
|
.ok_or_else(|| Error::cargo_error("Cargo.toml [members] is not an array"))?
|
|
|
|
.to_owned();
|
|
|
|
|
|
|
|
let vcs_path = work_dir.join("Cargo_vcs.toml");
|
|
|
|
let profiles_map = if vcs_path.exists() {
|
|
|
|
let vcs_contents = fs::read_to_string(vcs_path)?;
|
|
|
|
let vcs: toml::Value = toml::from_str(&vcs_contents)?;
|
|
|
|
let vcs_main = vcs
|
|
|
|
.as_table()
|
|
|
|
.ok_or_else(|| Error::cargo_error("Cargo_vcs.toml is not a yaml table"))?
|
|
|
|
.to_owned();
|
|
|
|
Self::find_profiles(&vcs_main)?
|
|
|
|
} else {
|
|
|
|
HashMap::new()
|
|
|
|
};
|
|
|
|
|
|
|
|
let profiles = profiles_map.keys().cloned().collect();
|
|
|
|
let projects = Self::process_repos(&work_dir, &members, &profiles_map)?;
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
work_dir,
|
|
|
|
profiles,
|
|
|
|
projects,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn list(&self) {
|
|
|
|
for project in self.projects() {
|
|
|
|
println!("{}", project);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn status(&self) -> Result<(), Error> {
|
|
|
|
if let Some(profile_name) = self.current_profile()? {
|
|
|
|
println!(
|
|
|
|
"Workspace is set to profile: {} [{}] {}",
|
|
|
|
profile_name.bold().with(PROFILE_COLOR),
|
|
|
|
format!("{}", self.work_dir.display()).italic(),
|
|
|
|
self.min_msrv_str(),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
if self.profiles.is_empty() {
|
|
|
|
println!(
|
|
|
|
"{}",
|
|
|
|
"No profiles defined, use 'cargo vcs save' to generate one\n".with(ERROR_COLOR)
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
println!(
|
|
|
|
"{}",
|
|
|
|
"Mismatching projects and profiles\n".with(ERROR_COLOR)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
self.list();
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn projects(&self) -> Projects<'_> {
|
|
|
|
self.projects.iter()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn min_msrv_str(&self) -> String {
|
|
|
|
if let Some(msrv) = self.min_msrv() {
|
|
|
|
format!("MSRV: {}", msrv).with(MSRV_COLOR).to_string()
|
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn min_msrv(&self) -> Option<&Version> {
|
|
|
|
let mut min_msrv = None;
|
|
|
|
|
|
|
|
for project in &self.projects {
|
|
|
|
let msrv = project.msrv();
|
|
|
|
if min_msrv.is_none() || (msrv.is_some() && msrv < min_msrv) {
|
|
|
|
min_msrv = msrv;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
min_msrv
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn list_profiles(&self) -> Result<(), Error> {
|
|
|
|
let current_profile = self.current_profile()?;
|
|
|
|
let mut prefix = "";
|
|
|
|
print!("[");
|
|
|
|
|
|
|
|
for profile in &self.profiles {
|
|
|
|
if current_profile.as_ref() == Some(profile) {
|
|
|
|
print!("{}{}", prefix, profile.as_str().bold().with(PROFILE_COLOR));
|
|
|
|
} else {
|
|
|
|
print!("{}{}", prefix, profile.as_str().with(PROFILE_COLOR));
|
|
|
|
}
|
|
|
|
prefix = ", ";
|
|
|
|
}
|
|
|
|
|
|
|
|
println!("]");
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn save_profile(&mut self, profile_name: &str) -> Result<(), Error> {
|
|
|
|
let vcs_path = self.work_dir.join("Cargo_vcs.toml");
|
|
|
|
let vcs = match fs::read_to_string(&vcs_path) {
|
|
|
|
Ok(val) => toml::from_str(&val)?,
|
|
|
|
Err(_) => Value::Table(Map::new()),
|
|
|
|
};
|
|
|
|
let mut vcs_main = vcs
|
|
|
|
.as_table()
|
|
|
|
.ok_or_else(|| Error::cargo_error("Cargo_vcs.toml is not a yaml table"))?
|
|
|
|
.to_owned();
|
|
|
|
|
|
|
|
let value = self.current_profile_toml_value()?;
|
|
|
|
if let Some(vcs_section) = vcs_main.get_mut("vcs") {
|
|
|
|
if let Some(section) = vcs_section.as_table_mut() {
|
|
|
|
section.insert(profile_name.into(), value);
|
|
|
|
} else {
|
|
|
|
return Err(Error::Upstream(Box::new(toml::ser::Error::Custom(
|
|
|
|
"Existing vcs section not a table".into(),
|
|
|
|
))));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let mut section = Table::new();
|
|
|
|
section.insert(profile_name.into(), value);
|
|
|
|
vcs_main.insert("vcs".into(), Value::Table(section));
|
|
|
|
}
|
|
|
|
|
|
|
|
let new_contents = toml::to_string(&vcs_main)?;
|
|
|
|
fs::write(vcs_path, new_contents)?;
|
|
|
|
|
|
|
|
if !&self.profiles.iter().any(|p| p == profile_name) {
|
|
|
|
self.profiles.push(profile_name.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
println!(
|
|
|
|
"Workspace state saved as {}",
|
|
|
|
profile_name.with(PROFILE_COLOR),
|
|
|
|
);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_profile(&mut self, profile_name: &str) -> Result<(), Error> {
|
|
|
|
if !self.profiles.contains(&String::from(profile_name)) {
|
|
|
|
eprintln!(
|
|
|
|
"{}{}",
|
|
|
|
"Profile not found: ".with(ERROR_COLOR),
|
|
|
|
profile_name.with(PROFILE_COLOR)
|
|
|
|
);
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
self.set_projects_using(profile_name, |project| {
|
|
|
|
project.switch_to_profile(profile_name)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_branch(&mut self, branch_name: &str) -> Result<(), Error> {
|
|
|
|
self.set_projects_using(branch_name, |project| project.repo.checkout(branch_name))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_projects_using<F>(&mut self, dest_name: &str, setter: F) -> Result<(), Error>
|
|
|
|
where
|
|
|
|
F: Fn(&mut Project) -> Result<String, Error>,
|
|
|
|
{
|
|
|
|
for project in &mut self.projects {
|
|
|
|
let current_ref = project.repo.current_ref()?;
|
|
|
|
// stash changes before moving project off to a new ref
|
|
|
|
let stashed_changes = project.repo.stash_changes(¤t_ref)?;
|
|
|
|
|
|
|
|
// try to switch to new ref using provided setter
|
|
|
|
let new_ref = match setter(project) {
|
|
|
|
Ok(val) => val,
|
|
|
|
Err(err) => {
|
|
|
|
eprintln!(
|
|
|
|
"{} unable to set {} ({})",
|
|
|
|
project.name().bold().with(PROJECT_COLOR),
|
|
|
|
dest_name.with(REFS_COLOR),
|
|
|
|
err.to_string().with(ERROR_COLOR),
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// unstash changes if we had previously stashed any on this ref
|
|
|
|
let unstashed_changes = project.repo.unstash_changes(&new_ref)?;
|
|
|
|
|
|
|
|
println!(
|
|
|
|
"{} set to {} {}{}",
|
|
|
|
project.name().bold().with(PROJECT_COLOR),
|
|
|
|
new_ref.with(REFS_COLOR),
|
|
|
|
if stashed_changes {
|
|
|
|
"«".cyan()
|
|
|
|
} else {
|
|
|
|
"".bold()
|
|
|
|
},
|
|
|
|
if unstashed_changes {
|
|
|
|
"»".dark_blue()
|
|
|
|
} else {
|
|
|
|
"".bold()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn current_profile_toml_value(&self) -> Result<Value, Error> {
|
|
|
|
let mut table = Table::new();
|
|
|
|
|
|
|
|
for project in &self.projects {
|
|
|
|
let head_ref_name = project.repo.current_ref()?;
|
|
|
|
table.insert(project.name().into(), Value::String(head_ref_name));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Value::Table(table))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn current_profile(&self) -> Result<Option<String>, Error> {
|
|
|
|
let mut found_profile = None;
|
|
|
|
for project in &self.projects {
|
|
|
|
match project.current_profile()? {
|
|
|
|
None => return Ok(None), // gap
|
|
|
|
Some(project_profile) => match &mut found_profile {
|
|
|
|
Some(previous) => {
|
|
|
|
if previous != &project_profile {
|
|
|
|
return Ok(None); // mismatch
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
found_profile.replace(project_profile);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(found_profile)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn find_profiles(
|
|
|
|
vcs_main: &Map<String, Value>,
|
|
|
|
) -> Result<HashMap<String, HashMap<String, String>>, Error> {
|
|
|
|
let mut result = HashMap::new();
|
|
|
|
|
|
|
|
if let Some(vcs_section) = vcs_main.get("vcs") {
|
|
|
|
if let Some(vcs) = vcs_section.as_table() {
|
|
|
|
for profile in vcs {
|
|
|
|
if let Some(repo_to_branch) = profile.1.as_table() {
|
|
|
|
let mut names = HashMap::new();
|
|
|
|
for rtb in repo_to_branch {
|
|
|
|
let project_name = rtb.0.clone();
|
|
|
|
let branch_name = rtb.1.as_str().ok_or_else(|| {
|
|
|
|
Error::project_error("Invalid UTF-8 in branch name")
|
|
|
|
})?;
|
|
|
|
names.insert(project_name, branch_name.to_owned());
|
|
|
|
}
|
|
|
|
|
|
|
|
let profile_name = profile.0.clone();
|
|
|
|
result.insert(profile_name, names);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process_repos(
|
|
|
|
work_dir: &Path,
|
|
|
|
members: &Vec<Value>,
|
|
|
|
profiles: &HashMap<String, HashMap<String, String>>,
|
|
|
|
) -> Result<Vec<Project>, Error> {
|
|
|
|
let mut projects = Vec::new();
|
|
|
|
for member in members {
|
|
|
|
let repo_postfix = member
|
|
|
|
.as_str()
|
|
|
|
.ok_or_else(|| Error::project_error("Repo not a string"))?;
|
|
|
|
let repo_path = work_dir.join(repo_postfix);
|
|
|
|
|
|
|
|
let repo_basename = Path::new(repo_postfix)
|
|
|
|
.components()
|
|
|
|
.last()
|
|
|
|
.ok_or_else(|| Error::project_error("Repo basename not found"))?
|
|
|
|
.as_os_str()
|
|
|
|
.to_str()
|
|
|
|
.ok_or_else(|| Error::project_error("Repo basename not valid UTF-8"))?;
|
|
|
|
|
|
|
|
let mut profile_map = HashMap::new();
|
|
|
|
|
|
|
|
for profile in profiles {
|
|
|
|
if let Some(branch_name) = profile.1.get(repo_basename) {
|
|
|
|
profile_map.insert(profile.0.clone(), branch_name.clone());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
projects.push(Project::new(repo_path, profile_map)?);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(projects)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn search(path: &Path) -> Result<PathBuf, Error> {
|
|
|
|
let path_str = path.to_str().expect("Invalid UTF-8 in path");
|
|
|
|
if !path.exists() {
|
|
|
|
return Err(Error::input_error("Search path not found", path_str));
|
|
|
|
}
|
|
|
|
|
|
|
|
if !path.is_dir() {
|
|
|
|
return Err(Error::input_error("Search path not a directory", path_str));
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut path = path;
|
|
|
|
loop {
|
|
|
|
let try_cargo = path.join("Cargo.toml");
|
|
|
|
if try_cargo.exists() && Self::get_workspace_toml(&try_cargo).is_ok() {
|
|
|
|
return Ok(try_cargo);
|
|
|
|
} else {
|
|
|
|
path = path
|
|
|
|
.parent()
|
|
|
|
.ok_or_else(|| Error::input_error("No workspace found", path_str))?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_workspace_toml(path: &Path) -> Result<Map<String, Value>, Error> {
|
|
|
|
let toml_contents = fs::read_to_string(path)?;
|
|
|
|
let cargo: toml::Value = toml::from_str(&toml_contents)?;
|
|
|
|
|
|
|
|
let cargo_main = cargo
|
|
|
|
.as_table()
|
|
|
|
.ok_or_else(|| Error::cargo_error("Cargo.toml is not a yaml table"))?
|
|
|
|
.to_owned();
|
|
|
|
|
|
|
|
let workspace = cargo_main
|
|
|
|
.get("workspace")
|
|
|
|
.ok_or_else(|| Error::cargo_error("Cargo.toml is not a workspace"))?
|
|
|
|
.as_table()
|
|
|
|
.ok_or_else(|| Error::cargo_error("Cargo.toml [workspace] is not a table"))?
|
|
|
|
.to_owned();
|
|
|
|
|
|
|
|
Ok(workspace)
|
|
|
|
}
|
|
|
|
}
|