diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c42e4e..e474fa60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ ## Other +- When saving/reading user-provided syntaxes or themes, `bat` will now maintain a + `metadata.yaml` file which includes information about the `bat` version which was + used to create the cached files. When loading cached files, we now print an error + if they have been created with an incompatible version. See #882 - Updated `liquid` dependency to 0.20, see #880 (@ignatenkobrain) ## `bat` as a library diff --git a/Cargo.lock b/Cargo.lock index 2568f3fa..33d3cad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,9 @@ dependencies = [ "globset 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "liquid 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)", "shell-words 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "syntect 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -384,6 +387,11 @@ name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "dtoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "either" version = "1.5.3" @@ -1117,6 +1125,19 @@ dependencies = [ "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "serde" version = "1.0.106" @@ -1145,6 +1166,17 @@ dependencies = [ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serde_yaml" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -1494,6 +1526,7 @@ dependencies = [ "checksum dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" "checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" "checksum doc-comment 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" "checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" "checksum encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" "checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" @@ -1585,9 +1618,12 @@ dependencies = [ "checksum ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" "checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" "checksum same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" "checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" "checksum serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" "checksum serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" +"checksum serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)" = "691b17f19fc1ec9d94ec0b5864859290dff279dbd7b03f017afda54eb36c3c35" "checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" "checksum shell-words 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "39acde55a154c4cd3ae048ac78cc21c25f3a0145e44111b523279113dce0d94a" "checksum shlex 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" diff --git a/Cargo.toml b/Cargo.toml index 65fdba02..c800a2da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,9 @@ encoding = "0.2" shell-words = { version = "0.1.0", optional = true } unicode-width = "0.1.7" globset = "0.4" +serde = "1.0" +serde_yaml = "0.8" +semver = "0.9" [dependencies.git2] version = "0.13" diff --git a/src/assets.rs b/src/assets.rs index 6aca40be..c75641de 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -8,6 +8,7 @@ use syntect::dumps::{dump_to_file, from_binary, from_reader}; use syntect::highlighting::{Theme, ThemeSet}; use syntect::parsing::{SyntaxReference, SyntaxSet, SyntaxSetBuilder}; +use crate::assets_metadata::AssetsMetadata; use crate::errors::*; use crate::inputfile::{InputFile, InputFileReader}; use crate::syntax_mapping::{MappingTarget, SyntaxMapping}; @@ -68,8 +69,11 @@ impl HighlightingAssets { }) } - pub fn from_cache(theme_set_path: &Path, syntax_set_path: &Path) -> Result { - let syntax_set_file = File::open(syntax_set_path).chain_err(|| { + pub fn from_cache(cache_path: &Path) -> Result { + let syntax_set_path = cache_path.join("syntaxes.bin"); + let theme_set_path = cache_path.join("themes.bin"); + + let syntax_set_file = File::open(&syntax_set_path).chain_err(|| { format!( "Could not load cached syntax set '{}'", syntax_set_path.to_string_lossy() @@ -142,6 +146,13 @@ impl HighlightingAssets { })?; println!("okay"); + print!( + "Writing metadata to folder {} ... ", + target_dir.to_string_lossy() + ); + AssetsMetadata::new().save_to_folder(target_dir)?; + println!("okay"); + Ok(()) } diff --git a/src/assets_metadata.rs b/src/assets_metadata.rs new file mode 100644 index 00000000..ca5443c3 --- /dev/null +++ b/src/assets_metadata.rs @@ -0,0 +1,82 @@ +use std::fs::File; +use std::path::Path; +use std::time::SystemTime; + +use clap::crate_version; +use semver::Version; +use serde::{Deserialize, Serialize}; + +use crate::errors::*; + +#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct AssetsMetadata { + bat_version: Option, + creation_time: Option, +} + +const FILENAME: &'static str = "metadata.yaml"; + +impl AssetsMetadata { + pub(crate) fn new() -> AssetsMetadata { + AssetsMetadata { + bat_version: Some(crate_version!().into()), + creation_time: Some(SystemTime::now()), + } + } + + pub(crate) fn save_to_folder(&self, path: &Path) -> Result<()> { + let file = File::create(path.join(FILENAME))?; + serde_yaml::to_writer(file, self)?; + + Ok(()) + } + + fn try_load_from_folder(path: &Path) -> Result { + let file = File::open(path.join(FILENAME))?; + Ok(serde_yaml::from_reader(file)?) + } + + /// Load metadata about the stored cache file from the given folder. + /// + /// There are several possibilities: + /// - We find a metadata.yaml file and are able to parse it + /// => return the contained information + /// - We find a metadata.yaml file and but are not able to parse it + /// => return a SerdeYamlError + /// - We do not find a metadata.yaml file but a syntaxes.bin or themes.bin file + /// => assume that these were created by an old version of bat and return + /// AssetsMetadata::default() without version information + /// - We do not find a metadata.yaml file and no cached assets + /// => no user provided assets are available, return None + pub fn load_from_folder(path: &Path) -> Result> { + match Self::try_load_from_folder(path) { + Ok(metadata) => Ok(Some(metadata)), + Err(e) => match e.kind() { + ErrorKind::SerdeYamlError(_) => Err(e), + _ => { + if path.join("syntaxes.bin").exists() || path.join("themes.bin").exists() { + Ok(Some(Self::default())) + } else { + Ok(None) + } + } + }, + } + } + + pub fn is_compatible(&self) -> bool { + let current_version = + Version::parse(crate_version!()).expect("bat follows semantic versioning"); + let stored_version = self + .bat_version + .as_ref() + .and_then(|ver| Version::parse(ver).ok()); + + if let Some(stored_version) = stored_version { + current_version.major == stored_version.major + && current_version.minor == stored_version.minor + } else { + false + } + } +} diff --git a/src/bin/bat/assets.rs b/src/bin/bat/assets.rs index ed07bdb3..5d15ba1c 100644 --- a/src/bin/bat/assets.rs +++ b/src/bin/bat/assets.rs @@ -1,18 +1,12 @@ use std::borrow::Cow; use std::fs; -use std::path::PathBuf; + +use clap::crate_version; use crate::directories::PROJECT_DIRS; -use bat::HighlightingAssets; - -fn theme_set_path() -> PathBuf { - PROJECT_DIRS.cache_dir().join("themes.bin") -} - -fn syntax_set_path() -> PathBuf { - PROJECT_DIRS.cache_dir().join("syntaxes.bin") -} +use bat::errors::*; +use bat::{AssetsMetadata, HighlightingAssets}; pub fn config_dir() -> Cow<'static, str> { PROJECT_DIRS.config_dir().to_string_lossy() @@ -23,16 +17,40 @@ pub fn cache_dir() -> Cow<'static, str> { } pub fn clear_assets() { + let theme_set_path = PROJECT_DIRS.cache_dir().join("themes.bin"); + let syntax_set_path = PROJECT_DIRS.cache_dir().join("syntaxes.bin"); + let metadata_file = PROJECT_DIRS.cache_dir().join("metadata.yaml"); + print!("Clearing theme set cache ... "); - fs::remove_file(theme_set_path()).ok(); + fs::remove_file(theme_set_path).ok(); println!("okay"); print!("Clearing syntax set cache ... "); - fs::remove_file(syntax_set_path()).ok(); + fs::remove_file(syntax_set_path).ok(); + println!("okay"); + + print!("Clearing metadata file ... "); + fs::remove_file(metadata_file).ok(); println!("okay"); } -pub fn assets_from_cache_or_binary() -> HighlightingAssets { - HighlightingAssets::from_cache(&theme_set_path(), &syntax_set_path()) - .unwrap_or(HighlightingAssets::from_binary()) +pub fn assets_from_cache_or_binary() -> Result { + let cache_dir = PROJECT_DIRS.cache_dir(); + if let Some(metadata) = AssetsMetadata::load_from_folder(&cache_dir)? { + if !metadata.is_compatible() { + return Err(format!( + "The binary caches for the user-customized syntaxes and themes \ + in '{}' are not compatible with this version of bat ({}). To solve this, \ + either rebuild the cache (bat cache --build) or remove \ + the custom syntaxes/themes (bat cache --clear).\n\ + For more information, see:\n\n \ + https://github.com/sharkdp/bat#adding-new-syntaxes--language-definitions", + cache_dir.to_string_lossy(), + crate_version!() + ) + .into()); + } + } + + Ok(HighlightingAssets::from_cache(&cache_dir).unwrap_or(HighlightingAssets::from_binary())) } diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 178c5c8c..ebd13a18 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -54,7 +54,7 @@ fn run_cache_subcommand(matches: &clap::ArgMatches) -> Result<()> { } pub fn list_languages(config: &Config) -> Result<()> { - let assets = assets_from_cache_or_binary(); + let assets = assets_from_cache_or_binary()?; let mut languages = assets .syntaxes() .iter() @@ -116,7 +116,7 @@ pub fn list_languages(config: &Config) -> Result<()> { } pub fn list_themes(cfg: &Config) -> Result<()> { - let assets = assets_from_cache_or_binary(); + let assets = assets_from_cache_or_binary()?; let mut config = cfg.clone(); let mut style = HashSet::new(); style.insert(StyleComponent::Plain); @@ -147,7 +147,7 @@ pub fn list_themes(cfg: &Config) -> Result<()> { } fn run_controller(config: &Config) -> Result { - let assets = assets_from_cache_or_binary(); + let assets = assets_from_cache_or_binary()?; let controller = Controller::new(&config, &assets); controller.run() } diff --git a/src/errors.rs b/src/errors.rs index 91b890d0..cabf4f2a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -7,18 +7,27 @@ error_chain! { SyntectError(::syntect::LoadingError); ParseIntError(::std::num::ParseIntError); GlobParsingError(::globset::Error); + SerdeYamlError(::serde_yaml::Error); } } pub fn default_error_handler(error: &Error) { + use ansi_term::Colour::Red; + match error { Error(ErrorKind::Io(ref io_error), _) if io_error.kind() == ::std::io::ErrorKind::BrokenPipe => { ::std::process::exit(0); } + Error(ErrorKind::SerdeYamlError(_), _) => { + eprintln!( + "{}: Error while parsing metadata.yaml file: {}", + Red.paint("[bat error]"), + error + ); + } _ => { - use ansi_term::Colour::Red; eprintln!("{}: {}", Red.paint("[bat error]"), error); } }; diff --git a/src/lib.rs b/src/lib.rs index 404ad49c..5ef4b8f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ #![recursion_limit = "1024"] pub(crate) mod assets; +pub(crate) mod assets_metadata; pub mod config; pub(crate) mod controller; mod decorations; @@ -19,5 +20,6 @@ mod terminal; pub(crate) mod wrap; pub use assets::HighlightingAssets; +pub use assets_metadata::AssetsMetadata; pub use controller::Controller; pub use printer::{InteractivePrinter, Printer, SimplePrinter};