You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

441 lines
14 KiB
Rust

3 years ago
use crossterm::style::Stylize;
use semver::Version;
3 years ago
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> {
3 years ago
if let Some(profile_name) = self.current_profile()? {
println!(
"Workspace is set to profile: {} [{}] {}",
3 years ago
profile_name.bold().with(PROFILE_COLOR),
format!("{}", self.work_dir.display()).italic(),
self.min_msrv_str(),
3 years ago
);
let mut once = true;
for project in &self.projects {
if once {
println!("\n{}\n", "** Projects with uncommitted changes **".italic());
once = false;
}
if !project.repo.uncommitted_changes()?.is_empty() {
println!(
"{}",
project.display(ProjectDisplayOptions::Changes.into())?
);
}
}
3 years ago
} else {
3 years ago
if self.profiles.is_empty() {
println!(
"{}",
"No profiles defined, use 'cargo vcs save' to generate one\n".with(ERROR_COLOR)
);
3 years ago
} else {
println!(
"{}",
"Mismatching projects and profiles\n".with(ERROR_COLOR)
);
3 years ago
}
3 years ago
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();
3 years ago
if min_msrv.is_none() || (msrv.is_some() && msrv < min_msrv) {
min_msrv = msrv;
}
}
min_msrv
}
3 years ago
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> {
3 years ago
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)?;
3 years ago
if !&self.profiles.iter().any(|p| p == profile_name) {
self.profiles.push(profile_name.into());
}
3 years ago
println!(
"Workspace state saved as {}",
3 years ago
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, None, |project| {
project.switch_to_profile(profile_name)
})
3 years ago
}
pub fn checkout_ref(
&mut self,
ref_name: &str,
projects: Option<Vec<String>>,
) -> Result<(), Error> {
self.set_projects_using(ref_name, projects, |project| {
project.repo.checkout(ref_name)
})
}
fn set_projects_using<F>(
&mut self,
dest_name: &str,
projects: Option<Vec<String>>,
setter: F,
) -> Result<(), Error>
where
F: Fn(&mut Project) -> Result<String, Error>,
{
for project in &mut self.projects {
if let Some(only_projects) = &projects {
if !only_projects.iter().any(|p| p == project.name()) {
continue;
}
}
let current_ref = project.repo.current_ref()?;
// stash changes before moving project off to a new ref
let stashed_changes = project.repo.stash_changes(&current_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)?;
3 years ago
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()
},
3 years ago
);
}
3 years ago
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()
3 years ago
.ok_or_else(|| Error::input_error("No workspace found", path_str))?;
3 years ago
}
}
}
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)
}
}