From 111ee5f3799f7687d0ae2453e078e1e26694a9ce Mon Sep 17 00:00:00 2001 From: Michal Date: Sun, 24 Jul 2022 16:07:32 +0100 Subject: [PATCH] Initial git_info and colorblind implementation --- Cargo.lock | 85 +++++++++++++++ Cargo.toml | 2 +- docs/WORKSPACE_MODE.md | 18 +++- examples/repository/mlc.toml | 2 - examples/workspace/mlc.toml | 11 +- src/internal/exit_codes.rs | 1 + src/internal/structs.rs | 12 ++- src/main.rs | 1 + src/operations/build.rs | 12 ++- src/operations/clean.rs | 3 +- src/operations/clone.rs | 4 +- src/operations/info.rs | 196 ++++++++++++++++++++++++++++++----- src/operations/pull.rs | 14 +-- src/repository/repo.rs | 8 +- 14 files changed, 311 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a6fe4f..98f9458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,30 @@ dependencies = [ "toml", ] +[[package]] +name = "ansi-str" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50acdf02a3ac61856d5c8d576a8b5fb452a6549f667ca29fefaa18c2cd05135" +dependencies = [ + "ansitok", +] + +[[package]] +name = "ansitok" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c6eb31f539d8fc1df948eb26452d6c781be4c9883663e7acb258644b71d5b1" +dependencies = [ + "nom", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "atty" version = "0.2.14" @@ -161,6 +185,12 @@ dependencies = [ "cc", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "mimalloc" version = "0.1.29" @@ -170,6 +200,22 @@ dependencies = [ "libmimalloc-sys", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "numtoa" version = "0.1.0" @@ -194,8 +240,10 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453cf71f2a37af495a1a124bf30d4d7469cfbea58e9f2479be9d222396a518a2" dependencies = [ + "ansi-str", "bytecount", "fnv", + "strip-ansi-escapes", "unicode-width", ] @@ -291,6 +339,15 @@ dependencies = [ "syn", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -314,6 +371,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5b2f8c37d26d87d2252187b0a45ea3cbf42baca10377c7e7eaaa2800fa9bf97" dependencies = [ + "ansi-str", "papergrid", "tabled_derive", "unicode-width", @@ -380,12 +438,39 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 9923a3f..e03621f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,6 @@ serde = { version = "1.0.139", default-features = false } serde_derive = { version = "1.0.139", default-features = false } libc = { version = "0.2.126", default-features = false } colored = { version = "2.0.0", default-features = false } -tabled = { version = "0.8.0", default-features = false, features = ["derive"] } +tabled = { version = "0.8.0", default-features = false, features = ["derive", "color"] } termion = { version = "1.5.6", default-features = false } regex = { version = "1.6.0", default-features = false, features = ["std"] } diff --git a/docs/WORKSPACE_MODE.md b/docs/WORKSPACE_MODE.md index 125bd02..5bfd573 100644 --- a/docs/WORKSPACE_MODE.md +++ b/docs/WORKSPACE_MODE.md @@ -5,10 +5,24 @@ You'll never have to work(space) another day in your life! ```toml [mode.workspace] - +git_info = true +colorblind = true ``` -Oh, this is awkward. It seems like there *is no* workspace-specific config yet. I'm open to suggestions though! +Currently, Workspace mode only has 2 options, both pertaining to the display of information. (`mlc info`) + +The first key is `git_info`, which is a boolean value. If it is true, the git information will be displayed alongside repository information. + +This information will be formatted as so: `D Pl Ps ` + +The key for the values is as follows: +- D: Whether the repository is dirty or not (unstaged changes) +- Pl: Whether there are unpulled changes at the remote +- Ps: Whether there are unpushed changes in your local repository + +These will be typically displayed in either Green (Clean) or Red (Dirty) + +However, if colorblind is set to true, the colors will instead be set to Blue (Clean) or Dark Red (Dirty), to be more discernible to colorblind users ### For Now... diff --git a/examples/repository/mlc.toml b/examples/repository/mlc.toml index d2d37a4..99387b0 100644 --- a/examples/repository/mlc.toml +++ b/examples/repository/mlc.toml @@ -11,8 +11,6 @@ enabled = true key = "michal@tar.black" on_gen = true -[mode.workspace] - [repositories] repos = [ "crs:malachite/development", diff --git a/examples/workspace/mlc.toml b/examples/workspace/mlc.toml index 7c1a7d6..db0466f 100644 --- a/examples/workspace/mlc.toml +++ b/examples/workspace/mlc.toml @@ -2,16 +2,9 @@ mode = "workspace" smart_pull = true -[mode.repository] -name = "" -build_on_update = false - -[mode.repository.signing] -enabled = false -key = "" -on_gen = false - [mode.workspace] +git_info = true +colorblind = true [repositories] repos = [ diff --git a/src/internal/exit_codes.rs b/src/internal/exit_codes.rs index f50199f..9e7ddb3 100644 --- a/src/internal/exit_codes.rs +++ b/src/internal/exit_codes.rs @@ -8,4 +8,5 @@ pub enum AppExitCode { NoPkgs = 7, ConfigParseError = 8, InvalidRepo = 9, + NotInit = 10, } diff --git a/src/internal/structs.rs b/src/internal/structs.rs index 5d83b84..0be443c 100755 --- a/src/internal/structs.rs +++ b/src/internal/structs.rs @@ -24,8 +24,8 @@ pub struct ConfigBase { #[derive(Debug, Deserialize)] pub struct ConfigMode { - pub repository: ConfigModeRepository, - pub workspace: ConfigModeWorkspace, + pub repository: Option, + pub workspace: Option, } #[derive(Debug, Deserialize)] @@ -43,7 +43,13 @@ pub struct ConfigModeRepositorySigning { } #[derive(Debug, Deserialize)] -pub struct ConfigModeWorkspace {} +pub struct ConfigModeWorkspace { + pub git_info: bool, + pub colorblind: bool, + /* pub backup: bool, + pub backup_dir: Option, TODO: Implement backup + */ +} #[derive(Debug, Deserialize)] pub struct ConfigRepositories { diff --git a/src/main.rs b/src/main.rs index 558b1ae..622d63f 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] +#![allow(clippy::too_many_lines)] use clap::Parser; use std::env; diff --git a/src/operations/build.rs b/src/operations/build.rs index 8b6d3dc..a42d3b9 100644 --- a/src/operations/build.rs +++ b/src/operations/build.rs @@ -8,12 +8,18 @@ pub fn build(packages: &[String], exclude: Vec, no_regen: bool, verbose: log!(verbose, "Config: {:?}", config); let all = packages.is_empty(); log!(verbose, "All: {:?}", all); - let sign = if config.mode.repository.signing.enabled && config.mode.repository.signing.on_gen { + let sign = if config.mode.repository.as_ref().unwrap().signing.enabled + && config.mode.repository.as_ref().unwrap().signing.on_gen + { false } else { - config.mode.repository.signing.enabled + config.mode.repository.as_ref().unwrap().signing.enabled }; - log!(verbose, "Signing: {:?}", config.mode.repository.signing); + log!( + verbose, + "Signing: {:?}", + config.mode.repository.unwrap().signing + ); // Get list of repos and subtract exclude let mut repos: Vec = config.repositories; diff --git a/src/operations/clean.rs b/src/operations/clean.rs index f454ada..1c9a65d 100644 --- a/src/operations/clean.rs +++ b/src/operations/clean.rs @@ -9,8 +9,9 @@ pub fn clean(verbose: bool) { .map(|x| x.unwrap().path().display().to_string()) .collect::>(); - // Remove all files/dirs in the current directory, excluding ./mlc.toml + // Remove all files/dirs in the current directory, excluding ./mlc.toml and .git dirs.retain(|x| *x != "./mlc.toml"); + dirs.retain(|x| *x != "./.git"); log!(verbose, "Paths with mlc.toml excluded: {:?}", dirs); for dir in dirs { std::fs::remove_dir_all(dir).unwrap(); diff --git a/src/operations/clone.rs b/src/operations/clone.rs index 62e6078..2872cec 100644 --- a/src/operations/clone.rs +++ b/src/operations/clone.rs @@ -16,7 +16,9 @@ pub fn clone(verbose: bool) { .collect::>(); dirs.retain(|x| *x != "./mlc.toml"); dirs.retain(|x| *x != "./out"); - dirs.retain(|x| *x != format!("./{}", config.mode.repository.name)); + if config.mode.repository.is_some() { + dirs.retain(|x| *x != format!("./{}", config.mode.repository.as_ref().unwrap().name)); + } log!(verbose, "Paths with mlc.toml excluded: {:?}", dirs); // Creates a vector of the difference between cloned repos and repos defined in config diff --git a/src/operations/info.rs b/src/operations/info.rs index 3ad7631..ecc7acf 100644 --- a/src/operations/info.rs +++ b/src/operations/info.rs @@ -1,46 +1,168 @@ -use crate::{info, log}; +use crate::{crash, info, internal::AppExitCode, log}; use colored::Colorize; use std::env; +use std::process::Command; use tabled::Tabled; // For displaying the table of contents +#[derive(Clone, tabled::Tabled, Debug)] +struct RepoDisplayGit { + #[tabled(rename = "Name")] + name: String, + #[tabled(rename = "URL")] + url: String, + #[tabled(skip)] + priority: usize, + #[tabled(rename = "Git Info")] + git_info: String, +} + #[derive(Clone, tabled::Tabled, Debug)] struct RepoDisplay { #[tabled(rename = "Name")] name: String, #[tabled(rename = "URL")] url: String, - #[tabled(rename = "Priority")] + #[tabled(skip)] priority: usize, } +pub fn git_status(verbose: bool, repo: &str, colorblind: bool) -> String { + let dir = env::current_dir().unwrap(); + log!( + verbose, + "Current directory: {}", + env::current_dir().unwrap().display() + ); + env::set_current_dir(&repo).unwrap_or_else(|e| { + crash!( + AppExitCode::NotInit, + "Failed to enter directory {} for Git info: {}", + repo, + e.to_string() + ); + }); + log!(verbose, "Current directory: {}", repo); + + Command::new("git") + .args(&["remote", "update"]) + .output() + .unwrap(); + + let output = Command::new("git").arg("status").output().unwrap(); + let output = String::from_utf8(output.stdout).unwrap(); + log!(verbose, "Git status: {}", output); + + let unstaged = output.contains("Changes not staged for commit"); + let untracked = output.contains("Untracked files"); + let dirty = unstaged || untracked; + + let pull = output.contains("Your branch is behind"); + let push = output.contains("Your branch is ahead"); + + let latest_commit = Command::new("git") + .args(&["log", "--pretty=%h", "-1"]) + .output() + .unwrap(); + let mut latest_commit = String::from_utf8(latest_commit.stdout).unwrap(); + latest_commit.retain(|c| !c.is_whitespace()); + + let output = if colorblind { + format!( + "{} {} {} {}", + if dirty { "D".red() } else { "D".bright_blue() }, + if pull { "Pl".red() } else { "Pl".bright_blue() }, + if push { "Ps".red() } else { "Ps".bright_blue() }, + latest_commit + ) + } else { + format!( + "{} {} {} {}", + if dirty { "D".red() } else { "D".green() }, + if pull { "Pl".red() } else { "Pl".green() }, + if push { "Ps".red() } else { "Ps".green() }, + latest_commit + ) + }; + env::set_current_dir(&dir).unwrap(); + log!(verbose, "Current directory: {}", dir.display()); + output +} + pub fn info(verbose: bool) { log!(verbose, "Showing Info"); let config = crate::internal::parse_cfg(verbose); log!(verbose, "Config: {:?}", config); + let git_info = if config.mode.workspace.is_some() { + config.mode.workspace.as_ref().unwrap().git_info + } else { + false + }; + log!(verbose, "Git info: {}", git_info); + + let colorblind = if config.mode.workspace.is_some() { + config.mode.workspace.as_ref().unwrap().colorblind + } else { + false + }; + log!(verbose, "Colorblind: {}", colorblind); + // Add the branch to the name if it's not the default branch for said repository let repos_unparsed = config.repositories; let mut repos = vec![]; + let mut repos_git = vec![]; for repo in repos_unparsed { + // Get name with branch, '/' serving as the delimiter let name = if repo.branch.is_some() { format!("{}/{}", repo.name, repo.branch.unwrap()) } else { repo.name.clone() }; - repos.push(RepoDisplay { - name, - url: repo.url, - priority: repo.priority, - }); + + // Get git info, if applicable + let git_info_string = if git_info { + Some(git_status( + verbose, + &repo.name, + config.mode.workspace.as_ref().unwrap().colorblind, + )) + } else { + None + }; + + // Push to the correct vector, we're using a separate vector for git info because + // the struct we're displaying is different + if git_info { + repos_git.push(RepoDisplayGit { + name, + url: repo.url.clone(), + priority: repo.priority, + git_info: git_info_string.unwrap(), + }); + } else { + repos.push(RepoDisplay { + name, + url: repo.url.clone(), + priority: repo.priority, + }); + } } log!(verbose, "Repos: {:?}", repos); + // Sort by priority repos.sort_by(|a, b| b.priority.cmp(&a.priority)); - log!(verbose, "Repos Sorted: {:?}", repos); + repos_git.sort_by(|a, b| b.priority.cmp(&a.priority)); + if git_info { + log!(verbose, "Repos Sorted: {:?}", repos_git); + } else { + log!(verbose, "Repos Sorted: {:?}", repos); + } // Displaying basic info about the Malachite Repository - let internal_name = if config.mode.repository.name.is_empty() { + let internal_name = if config.mode.repository.is_none() + || config.mode.repository.as_ref().unwrap().name.is_empty() + { env::current_dir() .unwrap() .file_name() @@ -49,17 +171,13 @@ pub fn info(verbose: bool) { .unwrap() .to_string() } else { - config.mode.repository.name + config.mode.repository.unwrap().name }; let name = format!( "{} \"{}\":", - if config.base.mode == "repository" { - "Repository".to_string() - } else if config.base.mode == "workspace" { - "Workspace".to_string() - } else { - "".to_string() - }, + // Sidenote: It should NOT be this convoluted to capitalise the first character of a string in rust. What the fuck. + String::from_utf8_lossy(&[config.base.mode.as_bytes()[0].to_ascii_uppercase()]) + + &config.base.mode[1..], internal_name ); @@ -70,16 +188,42 @@ pub fn info(verbose: bool) { }; // Create table for displaying info - let table = tabled::Table::new(&repos) - .with(tabled::Style::modern()) - .with(tabled::Width::wrap(width as usize)) - .to_string(); + let table = if git_info { + tabled::Table::new(&repos_git) + .with(tabled::Style::modern()) + .with(tabled::Width::wrap(width as usize)) + .to_string() + } else { + tabled::Table::new(&repos) + .with(tabled::Style::modern()) + .with(tabled::Width::wrap(width as usize)) + .to_string() + }; + + // Get length of Vec for displaying in the table + let len = if git_info { + repos_git.len() + } else { + repos.len() + }; // Print all of the info info!("{}", name); - info!( - "Local Repositories: {}", - repos.len().to_string().green().bold() - ); - println!("{}", table.bold()); + info!("Local Repositories: {}", len); + println!("{}", table); + if config.mode.workspace.is_some() && config.mode.workspace.as_ref().unwrap().git_info { + info!( + "Key: \n \ + D: Dirty - Unstaged Changes \n \ + Pl: Pull - Changes at Remote \n \ + Ps: Push - Unpushed Changes \n \ + {}: Applies, {}: Does Not Apply", + " ".on_red(), + if config.mode.workspace.unwrap().colorblind { + " ".on_bright_blue() + } else { + " ".on_green() + } + ); + } } diff --git a/src/operations/pull.rs b/src/operations/pull.rs index 238cdcb..e74c97a 100644 --- a/src/operations/pull.rs +++ b/src/operations/pull.rs @@ -10,11 +10,7 @@ struct PullParams { no_regen: bool, } -fn do_the_pulling( - repos: Vec, - verbose: bool, - params: &PullParams, -) { +fn do_the_pulling(repos: Vec, verbose: bool, params: &PullParams) { for repo in repos { // Set root dir to return after each git pull let root_dir = env::current_dir().unwrap(); @@ -104,7 +100,11 @@ pub fn pull(packages: Vec, exclude: &[String], verbose: bool, no_regen: let smart_pull = config.base.smart_pull; log!(verbose, "Smart pull: {}", smart_pull); // Read build_on_update from config - let build_on_update = config.mode.repository.build_on_update; + let build_on_update = if config.mode.repository.is_some() { + config.mode.repository.unwrap().build_on_update + } else { + false + }; log!(verbose, "Build on update: {}", build_on_update); // Read repos from config @@ -142,6 +142,6 @@ pub fn pull(packages: Vec, exclude: &[String], verbose: bool, no_regen: smart_pull, build_on_update, no_regen, - } + }, ); } diff --git a/src/repository/repo.rs b/src/repository/repo.rs index 0d0b391..8fab7c8 100644 --- a/src/repository/repo.rs +++ b/src/repository/repo.rs @@ -10,7 +10,7 @@ pub fn generate(verbose: bool) { log!(verbose, "Config: {:?}", config); // Get repository name from config - let name = config.mode.repository.name; + let name = &config.mode.repository.as_ref().unwrap().name; log!(verbose, "Name: {}", name); info!("Generating repository: {}", name); @@ -39,7 +39,9 @@ pub fn generate(verbose: bool) { log!(verbose, "Current dir: {:?}", env::current_dir().unwrap()); // Sign all package files in repository if signing and on_gen are true - if config.mode.repository.signing.enabled && config.mode.repository.signing.on_gen { + if config.mode.repository.as_ref().unwrap().signing.enabled + && config.mode.repository.as_ref().unwrap().signing.on_gen + { // Get a list of all .tar.* files in repository let files = fs::read_dir("./").unwrap(); for file in files { @@ -52,7 +54,7 @@ pub fn generate(verbose: bool) { "-c", &format!( "gpg --default-key {} --detach-sign {}", - config.mode.repository.signing.key, + config.mode.repository.as_ref().unwrap().signing.key, file.file_name().to_str().unwrap() ), ])