use std::{
cmp,
collections::{HashMap, HashSet},
convert::TryInto,
fmt, fs,
path::{Path, PathBuf},
result, sync,
};
use anyhow::{anyhow, bail, Context as ResultExt, Error, Result};
use indexmap::{indexmap, IndexMap};
use itertools::{Either, Itertools};
use lazy_static::lazy_static;
use maplit::hashmap;
use serde::{Deserialize, Serialize};
use url::Url;
use walkdir::WalkDir;
use crate::{
config::{Config, ExternalPlugin, GitReference, InlinePlugin, Plugin, Source, Template},
context::{LockContext as Context, Settings, SettingsExt},
util::{self, git, TempPath},
};
const MAX_THREADS: u32 = 8;
lazy_static! {
pub static ref DEFAULT_MATCHES: Vec<String> = vec_into![
"{{ name }}.plugin.zsh",
"{{ name }}.zsh",
"{{ name }}.sh",
"{{ name }}.zsh-theme",
"*.plugin.zsh",
"*.zsh",
"*.sh",
"*.zsh-theme"
];
}
lazy_static! {
pub static ref DEFAULT_APPLY: Vec<String> = vec_into!["source"];
}
lazy_static! {
pub static ref DEFAULT_TEMPLATES: IndexMap<String, Template> = indexmap_into! {
"PATH" => "export PATH=\"{{ dir }}:$PATH\"",
"path" => "path=( \"{{ dir }}\" $path )",
"fpath" => "fpath=( \"{{ dir }}\" $fpath )",
"source" => Template::from("source \"{{ file }}\"").each(true)
};
}
#[derive(Clone, Debug)]
struct LockedGitReference(git2::Oid);
#[derive(Clone, Debug)]
struct LockedSource {
dir: PathBuf,
file: Option<PathBuf>,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct LockedExternalPlugin {
name: String,
source_dir: PathBuf,
plugin_dir: Option<PathBuf>,
files: Vec<PathBuf>,
apply: Vec<String>,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
#[serde(untagged)]
enum LockedPlugin {
External(LockedExternalPlugin),
Inline(InlinePlugin),
}
#[derive(Debug, Deserialize, Serialize)]
pub struct LockedConfig {
#[serde(flatten)]
pub settings: Settings,
plugins: Vec<LockedPlugin>,
templates: IndexMap<String, Template>,
#[serde(skip)]
pub errors: Vec<Error>,
}
impl fmt::Display for GitReference {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Branch(s) | Self::Rev(s) | Self::Tag(s) => write!(f, "{}", s),
}
}
}
impl GitReference {
fn lock(&self, repo: &git2::Repository) -> Result<LockedGitReference> {
match self {
Self::Branch(s) => git::resolve_branch(repo, s),
Self::Rev(s) => git::resolve_rev(repo, s),
Self::Tag(s) => git::resolve_tag(repo, s),
}
.map(LockedGitReference)
}
}
impl fmt::Display for Source {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Git {
url,
reference: Some(reference),
} => write!(f, "{}@{}", url, reference),
Self::Git { url, .. } | Self::Remote { url } => write!(f, "{}", url),
Self::Local { dir } => write!(f, "{}", dir.display()),
}
}
}
impl Source {
fn lock_git(
ctx: &Context,
dir: PathBuf,
url: Url,
reference: Option<GitReference>,
) -> Result<LockedSource> {
let checkout = |repo, status| -> Result<()> {
match reference {
Some(reference) => {
git::checkout(&repo, reference.lock(&repo)?.0)?;
git::submodule_update(&repo).context("failed to recursively update")?;
status!(ctx, status, &format!("{}@{}", &url, reference));
}
None => {
git::submodule_update(&repo).context("failed to recursively update")?;
status!(ctx, status, &url);
}
}
Ok(())
};
if !ctx.reinstall {
if let Ok(repo) = git::open(&dir) {
checkout(repo, "Checked")?;
return Ok(LockedSource { dir, file: None });
}
}
let temp_dir = TempPath::new(&dir);
let repo = git::clone(&url, &temp_dir.path())?;
checkout(repo, "Cloned")?;
temp_dir
.rename(&dir)
.context("failed to rename temporary clone directory")?;
Ok(LockedSource { dir, file: None })
}
fn lock_remote(ctx: &Context, dir: PathBuf, file: PathBuf, url: Url) -> Result<LockedSource> {
if !ctx.reinstall && file.exists() {
status!(ctx, "Checked", &url);
return Ok(LockedSource {
dir,
file: Some(file),
});
}
let mut response =
util::download(url.clone()).with_context(s!("failed to download `{}`", url))?;
fs::create_dir_all(&dir).with_context(s!("failed to create dir `{}`", dir.display()))?;
let mut temp_file = TempPath::new(&file);
temp_file.write(&mut response).with_context(s!(
"failed to copy contents to `{}`",
temp_file.path().display()
))?;
temp_file
.rename(&file)
.context("failed to rename temporary download file")?;
status!(ctx, "Fetched", &url);
Ok(LockedSource {
dir,
file: Some(file),
})
}
fn lock_local(ctx: &Context, dir: PathBuf) -> Result<LockedSource> {
let dir = ctx.expand_tilde(dir);
if let Ok(glob) = glob::glob(&dir.to_string_lossy()) {
let mut directories: Vec<_> = glob
.filter_map(|result| {
if let Ok(dir) = result {
if dir.is_dir() {
return Some(dir);
}
}
None
})
.collect();
if directories.len() == 1 {
let dir = directories.remove(0);
status!(ctx, "Checked", dir.as_path());
Ok(LockedSource { dir, file: None })
} else {
Err(anyhow!(
"`{}` matches {} directories",
dir.display(),
directories.len()
))
}
} else if fs::metadata(&dir)
.with_context(s!("failed to find dir `{}`", dir.display()))?
.is_dir()
{
status!(ctx, "Checked", dir.as_path());
Ok(LockedSource { dir, file: None })
} else {
Err(anyhow!("`{}` is not a dir", dir.display()))
}
}
fn lock(self, ctx: &Context) -> Result<LockedSource> {
match self {
Self::Git { url, reference } => {
let mut dir = ctx.clone_dir().to_path_buf();
dir.push(
url.host_str()
.with_context(s!("URL `{}` has no host", url))?,
);
dir.push(url.path().trim_start_matches('/'));
Self::lock_git(ctx, dir, url, reference)
}
Self::Remote { url } => {
let mut dir = ctx.download_dir().to_path_buf();
dir.push(
url.host_str()
.with_context(s!("URL `{}` has no host", url))?,
);
let segments: Vec<_> = url
.path_segments()
.with_context(s!("URL `{}` is cannot-be-a-base", url))?
.collect();
let (base, rest) = segments.split_last().unwrap();
let base = if *base == "" { "index" } else { *base };
dir.push(rest.iter().collect::<PathBuf>());
let file = dir.join(base);
Self::lock_remote(ctx, dir, file, url)
}
Self::Local { dir } => Self::lock_local(ctx, dir),
}
}
}
impl ExternalPlugin {
fn match_globs(pattern: PathBuf, files: &mut Vec<PathBuf>) -> Result<bool> {
let mut matched = false;
let pattern = pattern.to_string_lossy();
let paths: glob::Paths =
glob::glob(&pattern).with_context(s!("failed to parse glob pattern `{}`", &pattern))?;
for path in paths {
files.push(
path.with_context(s!("failed to read path matched by pattern `{}`", &pattern))?,
);
matched = true;
}
Ok(matched)
}
fn lock(
self,
ctx: &Context,
source: LockedSource,
matches: &[String],
apply: &[String],
) -> Result<LockedExternalPlugin> {
Ok(if let Source::Remote { .. } = self.source {
let LockedSource { dir, file } = source;
LockedExternalPlugin {
name: self.name,
source_dir: dir,
plugin_dir: None,
files: vec![file.unwrap()],
apply: self.apply.unwrap_or_else(|| apply.to_vec()),
}
} else {
let mut templates = handlebars::Handlebars::new();
templates.set_strict_mode(true);
let mut data = hashmap! {
"root" => ctx
.root()
.to_str()
.context("root directory is not valid UTF-8")?,
"name" => &self.name
};
let source_dir = source.dir;
let plugin_dir = if let Some(dir) = self.dir {
let rendered = templates
.render_template(&dir, &data)
.with_context(s!("failed to render template `{}`", dir))?;
Some(source_dir.join(rendered))
} else {
None
};
let dir = plugin_dir.as_ref().unwrap_or(&source_dir);
let dir_as_str = dir
.to_str()
.context("plugin directory is not valid UTF-8")?;
data.insert("dir", dir_as_str);
data.insert("directory", dir_as_str);
let mut files = Vec::new();
if let Some(uses) = &self.uses {
for u in uses {
let rendered = templates
.render_template(u, &data)
.with_context(s!("failed to render template `{}`", u))?;
let pattern = dir.join(&rendered);
if !Self::match_globs(pattern, &mut files)? {
bail!("failed to find any files matching `{}`", &rendered);
};
}
} else {
for g in matches {
let rendered = templates
.render_template(g, &data)
.with_context(s!("failed to render template `{}`", g))?;
let pattern = dir.join(rendered);
if Self::match_globs(pattern, &mut files)? {
break;
}
}
}
LockedExternalPlugin {
name: self.name,
source_dir,
plugin_dir,
files,
apply: self.apply.unwrap_or_else(|| apply.to_vec()),
}
})
}
}
impl Config {
pub fn lock(self, ctx: &Context) -> Result<LockedConfig> {
let (externals, inlines): (Vec<_>, Vec<_>) = self
.plugins
.into_iter()
.enumerate()
.partition_map(|(index, plugin)| match plugin {
Plugin::External(plugin) => Either::Left((index, plugin)),
Plugin::Inline(plugin) => Either::Right((index, LockedPlugin::Inline(plugin))),
});
let mut map = IndexMap::new();
for (index, plugin) in externals {
map.entry(plugin.source.clone())
.or_insert_with(|| Vec::with_capacity(1))
.push((index, plugin));
}
let matches = &self.matches.as_ref().unwrap_or(&*DEFAULT_MATCHES);
let apply = &self.apply.as_ref().unwrap_or(&*DEFAULT_APPLY);
let count = map.len();
let mut errors = Vec::new();
let plugins = if count == 0 {
inlines
.into_iter()
.map(|(_, locked)| locked)
.collect::<Vec<_>>()
} else {
let thread_count = cmp::min(count.try_into().unwrap_or(MAX_THREADS), MAX_THREADS);
let mut pool = scoped_threadpool::Pool::new(thread_count);
let (tx, rx) = sync::mpsc::channel();
pool.scoped(|scoped| {
for (source, plugins) in map {
let tx = tx.clone();
scoped.execute(move || {
tx.send((|| {
let source_name = source.to_string();
let source = source
.lock(ctx)
.with_context(s!("failed to install source `{}`", source_name))?;
let mut locked = Vec::with_capacity(plugins.len());
for (index, plugin) in plugins {
let name = plugin.name.clone();
locked.push((
index,
plugin
.lock(ctx, source.clone(), matches, apply)
.with_context(s!("failed to install plugin `{}`", name)),
));
}
Ok(locked)
})())
.expect("oops! did main thread die?");
})
}
scoped.join_all();
});
rx
.iter()
.take(count)
.collect::<Vec<_>>()
.into_iter()
.filter_map(|result| match result {
Ok(ok) => Some(ok),
Err(err) => {
errors.push(err);
None
}
})
.flatten()
.collect::<Vec<_>>()
.into_iter()
.filter_map(|(index, result)| match result {
Ok(plugin) => Some((index, LockedPlugin::External(plugin))),
Err(err) => {
errors.push(err);
None
}
})
.chain(inlines.into_iter())
.sorted_by_key(|(index, _)| *index)
.map(|(_, locked)| locked)
.collect::<Vec<_>>()
};
Ok(LockedConfig {
settings: ctx.settings().clone(),
templates: self.templates,
errors,
plugins,
})
}
}
impl LockedExternalPlugin {
fn dir(&self) -> &Path {
self.plugin_dir.as_ref().unwrap_or(&self.source_dir)
}
}
impl LockedConfig {
pub fn from_path<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let locked: Self = toml::from_str(&String::from_utf8_lossy(
&fs::read(&path)
.with_context(s!("failed to read locked config from `{}`", path.display()))?,
))
.context("failed to deserialize locked config")?;
Ok(locked)
}
pub fn verify(&self, ctx: &Context) -> bool {
if &self.settings != ctx.settings() {
return false;
}
for plugin in &self.plugins {
match plugin {
LockedPlugin::External(plugin) => {
if !plugin.dir().exists() {
return false;
}
for file in &plugin.files {
if !file.exists() {
return false;
}
}
}
LockedPlugin::Inline(_) => {}
}
}
true
}
fn remove_path(ctx: &Context, path: &Path) -> Result<()> {
let path_replace_home = ctx.replace_home(path);
let path_display = &path_replace_home.display();
if path
.metadata()
.with_context(s!("failed to fetch metadata for `{}`", path_display))?
.is_dir()
{
fs::remove_dir_all(path)
.with_context(s!("failed to remove directory `{}`", path_display))?;
} else {
fs::remove_file(path).with_context(s!("failed to remove file `{}`", path_display))?;
}
warning_v!(ctx, "Removed", path_display);
Ok(())
}
pub fn clean(&self, ctx: &Context, warnings: &mut Vec<Error>) {
let clean_clone_dir = self.settings.clone_dir().starts_with(self.settings.root());
let clean_download_dir = self
.settings
.download_dir()
.starts_with(self.settings.root());
if !clean_clone_dir && !clean_download_dir {
return;
}
let mut source_dirs = HashSet::new();
let mut parent_dirs = HashSet::new();
let mut files = HashSet::new();
for plugin in &self.plugins {
if let LockedPlugin::External(locked) = plugin {
source_dirs.insert(locked.source_dir.as_path());
parent_dirs.extend(locked.dir().ancestors());
files.extend(locked.files.iter().filter_map(|f| {
if f.starts_with(self.settings.download_dir()) {
Some(f.as_path())
} else {
None
}
}));
}
}
parent_dirs.insert(self.settings.clone_dir());
parent_dirs.insert(self.settings.download_dir());
if clean_clone_dir {
for entry in WalkDir::new(self.settings.clone_dir())
.into_iter()
.filter_entry(|e| !source_dirs.contains(e.path()))
.filter_map(result::Result::ok)
.filter(|e| !parent_dirs.contains(e.path()))
{
if let Err(err) = Self::remove_path(ctx, entry.path()) {
warnings.push(err);
}
}
}
if clean_download_dir {
for entry in WalkDir::new(self.settings.download_dir())
.into_iter()
.filter_map(result::Result::ok)
.filter(|e| {
let p = e.path();
!files.contains(p) && !parent_dirs.contains(p)
})
{
if let Err(err) = Self::remove_path(ctx, entry.path()) {
warnings.push(err);
}
}
}
}
pub fn source(&self, ctx: &Context) -> Result<String> {
let mut templates_map: HashMap<&str, &Template> =
HashMap::with_capacity(DEFAULT_TEMPLATES.len() + self.templates.len());
for (name, template) in DEFAULT_TEMPLATES.iter() {
templates_map.insert(name, template);
}
for (name, template) in &self.templates {
templates_map.insert(name, template);
}
let mut templates = handlebars::Handlebars::new();
templates.set_strict_mode(true);
for (name, template) in &templates_map {
templates
.register_template_string(&name, &template.value)
.with_context(s!("failed to compile template `{}`", name))?;
}
let mut script = String::new();
for plugin in &self.plugins {
match plugin {
LockedPlugin::External(plugin) => {
for name in &plugin.apply {
let dir_as_str = plugin
.dir()
.to_str()
.context("plugin directory is not valid UTF-8")?;
let mut data = hashmap! {
"root" => self
.settings
.root()
.to_str()
.context("root directory is not valid UTF-8")?,
"name" => &plugin.name,
"dir" => dir_as_str,
"directory" => dir_as_str,
};
if templates_map.get(name.as_str()).unwrap().each {
for file in &plugin.files {
let as_str =
file.to_str().context("plugin file is not valid UTF-8")?;
data.insert("file", as_str);
data.insert("filename", as_str);
script.push_str(
&templates
.render(name, &data)
.with_context(s!("failed to render template `{}`", name))?,
);
script.push('\n');
}
} else {
script.push_str(
&templates
.render(name, &data)
.with_context(s!("failed to render template `{}`", name))?,
);
script.push('\n');
}
}
status_v!(ctx, "Rendered", &plugin.name);
}
LockedPlugin::Inline(plugin) => {
let data = hashmap! {
"root" => self
.settings
.root()
.to_str()
.context("root directory is not valid UTF-8")?,
"name" => &plugin.name,
};
script.push_str(
&templates
.render_template(&plugin.raw, &data)
.with_context(s!(
"failed to render inline plugin `{}`",
&plugin.name
))?,
);
script.push('\n');
status_v!(ctx, "Inlined", &plugin.name);
}
}
}
Ok(script)
}
pub fn to_path<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let path = path.as_ref();
fs::write(
path,
&toml::to_string(&self).context("failed to serialize locked config")?,
)
.with_context(s!("failed to write locked config to `{}`", path.display()))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::{
fs,
io::{self, Read, Write},
process::Command,
thread, time,
};
use url::Url;
fn git_clone_sheldon_test(temp: &tempfile::TempDir) -> git2::Repository {
let dir = temp.path();
Command::new("git")
.arg("clone")
.arg("https://github.com/rossmacarthur/sheldon-test")
.arg(&dir)
.output()
.expect("git clone rossmacarthur/sheldon-test");
git2::Repository::open(dir).expect("open sheldon-test git repository")
}
fn create_test_context(root: &Path) -> Context {
Context {
settings: Settings {
version: structopt::clap::crate_version!().to_string(),
home: "/".into(),
config_file: root.join("config.toml"),
lock_file: root.join("config.lock"),
clone_dir: root.join("repos"),
download_dir: root.join("downloads"),
root: root.to_path_buf(), },
output: crate::log::Output {
verbosity: crate::log::Verbosity::Quiet,
no_color: true,
},
reinstall: false,
}
}
fn read_file_contents(file: &Path) -> io::Result<String> {
let mut file = fs::File::open(file)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
#[test]
fn git_reference_to_string() {
assert_eq!(
GitReference::Branch("feature".to_string()).to_string(),
"feature"
);
assert_eq!(
GitReference::Rev("ad149784a".to_string()).to_string(),
"ad149784a"
);
assert_eq!(GitReference::Tag("0.2.3".to_string()).to_string(), "0.2.3");
}
#[test]
fn git_reference_lock_branch() {
let temp = tempfile::tempdir().expect("create temporary directory");
let repo = git_clone_sheldon_test(&temp);
let reference = GitReference::Branch("feature".to_string());
let locked = reference.lock(&repo).expect("lock git reference");
assert_eq!(
locked.0.to_string(),
"09ead574b20bb573ae0a53c1a5c546181cfa41c8"
);
let reference = GitReference::Branch("not-a-branch".to_string());
let error = reference.lock(&repo).unwrap_err();
assert_eq!(error.to_string(), "failed to find branch `not-a-branch`");
}
#[test]
fn git_reference_lock_rev() {
let temp = tempfile::tempdir().expect("create temporary directory");
let repo = git_clone_sheldon_test(&temp);
let reference = GitReference::Rev("ad149784a".to_string());
let locked = reference.lock(&repo).unwrap();
assert_eq!(
locked.0.to_string(),
"ad149784a1538291f2477fb774eeeed4f4d29e45"
);
let reference = GitReference::Rev("2c4ed7710".to_string());
let error = reference.lock(&repo).unwrap_err();
assert_eq!(error.to_string(), "failed to find revision `2c4ed7710`");
}
#[test]
fn git_reference_lock_tag() {
let temp = tempfile::tempdir().expect("create temporary directory");
let repo = git_clone_sheldon_test(&temp);
let reference = GitReference::Tag("v0.1.0".to_string());
let locked = reference.lock(&repo).unwrap();
assert_eq!(
locked.0.to_string(),
"be8fde277e76f35efbe46848fb352cee68549962"
);
let reference = GitReference::Tag("v0.2.0".to_string());
let error = reference.lock(&repo).unwrap_err();
assert_eq!(error.to_string(), "failed to find tag `v0.2.0`");
}
#[test]
fn source_to_string() {
assert_eq!(
Source::Git {
url: Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: Some(GitReference::Tag("v0.3.0".to_string())),
}
.to_string(),
"https://github.com/rossmacarthur/sheldon-test@v0.3.0"
);
assert_eq!(
Source::Git {
url: Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: None,
}
.to_string(),
"https://github.com/rossmacarthur/sheldon-test"
);
assert_eq!(
Source::Remote {
url: Url::parse("https://github.com/rossmacarthur/sheldon/raw/0.3.0/LICENSE-MIT")
.unwrap(),
}
.to_string(),
"https://github.com/rossmacarthur/sheldon/raw/0.3.0/LICENSE-MIT"
);
assert_eq!(
Source::Local {
dir: PathBuf::from("~/plugins")
}
.to_string(),
"~/plugins"
);
}
#[test]
fn source_lock_git_and_reinstall() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let mut ctx = create_test_context(dir);
let url = Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap();
let locked = Source::lock_git(&ctx, dir.to_path_buf(), url.clone(), None).unwrap();
assert_eq!(locked.dir, dir);
assert_eq!(locked.file, None);
let repo = git2::Repository::open(&dir).unwrap();
assert_eq!(
repo.head().unwrap().target().unwrap().to_string(),
"be8fde277e76f35efbe46848fb352cee68549962"
);
let modified = fs::metadata(&dir).unwrap().modified().unwrap();
thread::sleep(time::Duration::from_secs(1));
ctx.reinstall = true;
let locked = Source::lock_git(&ctx, dir.to_path_buf(), url, None).unwrap();
assert_eq!(locked.dir, dir);
assert_eq!(locked.file, None);
let repo = git2::Repository::open(&dir).unwrap();
assert_eq!(
repo.head().unwrap().target().unwrap().to_string(),
"be8fde277e76f35efbe46848fb352cee68549962"
);
assert!(fs::metadata(&dir).unwrap().modified().unwrap() > modified);
}
#[test]
fn source_lock_git_with_reference() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let locked = Source::lock_git(
&create_test_context(dir),
dir.to_path_buf(),
Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap(),
Some(GitReference::Rev(
"ad149784a1538291f2477fb774eeeed4f4d29e45".to_string(),
)),
)
.unwrap();
assert_eq!(locked.dir, dir);
assert_eq!(locked.file, None);
let repo = git2::Repository::open(&dir).unwrap();
let head = repo.head().unwrap();
assert_eq!(
head.target().unwrap().to_string(),
"ad149784a1538291f2477fb774eeeed4f4d29e45"
)
}
#[test]
fn source_lock_git_with_git() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let locked = Source::lock_git(
&create_test_context(dir),
dir.to_path_buf(),
Url::parse("git://github.com/rossmacarthur/sheldon-test").unwrap(),
Some(GitReference::Rev(
"ad149784a1538291f2477fb774eeeed4f4d29e45".to_string(),
)),
)
.unwrap();
assert_eq!(locked.dir, dir);
assert_eq!(locked.file, None);
let repo = git2::Repository::open(&dir).unwrap();
let head = repo.head().unwrap();
assert_eq!(
head.target().unwrap().to_string(),
"ad149784a1538291f2477fb774eeeed4f4d29e45"
)
}
#[test]
fn source_lock_remote_and_reinstall() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let file = dir.join("test.txt");
let mut ctx = create_test_context(dir);
let url =
Url::parse("https://github.com/rossmacarthur/sheldon/raw/0.3.0/LICENSE-MIT").unwrap();
let locked =
Source::lock_remote(&ctx, dir.to_path_buf(), file.clone(), url.clone()).unwrap();
assert_eq!(locked.dir, dir);
assert_eq!(locked.file, Some(file.clone()));
assert_eq!(
read_file_contents(&file).unwrap(),
read_file_contents(&manifest_dir.join("LICENSE-MIT")).unwrap()
);
let modified = fs::metadata(&file).unwrap().modified().unwrap();
thread::sleep(time::Duration::from_secs(1));
ctx.reinstall = true;
let locked = Source::lock_remote(&ctx, dir.to_path_buf(), file.clone(), url).unwrap();
assert_eq!(locked.dir, dir);
assert_eq!(locked.file, Some(file.clone()));
assert_eq!(
read_file_contents(&file).unwrap(),
read_file_contents(&manifest_dir.join("LICENSE-MIT")).unwrap()
);
assert!(fs::metadata(&file).unwrap().modified().unwrap() > modified)
}
#[test]
fn source_lock_local() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let _ = git_clone_sheldon_test(&temp);
let locked = Source::lock_local(&create_test_context(dir), dir.to_path_buf()).unwrap();
assert_eq!(locked.dir, dir);
assert_eq!(locked.file, None);
}
#[test]
fn source_lock_with_git() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let ctx = create_test_context(dir);
let source = Source::Git {
url: Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: None,
};
let locked = source.lock(&ctx).unwrap();
assert_eq!(
locked.dir,
dir.join("repos/github.com/rossmacarthur/sheldon-test")
);
assert_eq!(locked.file, None)
}
#[test]
fn source_lock_with_remote() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let ctx = create_test_context(dir);
let source = Source::Remote {
url: Url::parse("https://github.com/rossmacarthur/sheldon/raw/0.3.0/LICENSE-MIT")
.unwrap(),
};
let locked = source.lock(&ctx).unwrap();
assert_eq!(
locked.dir,
dir.join("downloads/github.com/rossmacarthur/sheldon/raw/0.3.0")
);
assert_eq!(
locked.file,
Some(dir.join("downloads/github.com/rossmacarthur/sheldon/raw/0.3.0/LICENSE-MIT"))
);
}
#[test]
fn external_plugin_lock_git_with_uses() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let ctx = create_test_context(dir);
let plugin = ExternalPlugin {
name: "test".to_string(),
source: Source::Git {
url: Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: Some(GitReference::Tag("v0.1.0".to_string())),
},
dir: None,
uses: Some(vec!["*.md".into(), "{{ name }}.plugin.zsh".into()]),
apply: None,
};
let locked_source = plugin.source.clone().lock(&ctx).unwrap();
let clone_dir = dir.join("repos/github.com/rossmacarthur/sheldon-test");
let locked = plugin
.lock(&ctx, locked_source, &[], &["hello".into()])
.unwrap();
assert_eq!(locked.name, String::from("test"));
assert_eq!(locked.dir(), clone_dir);
assert_eq!(
locked.files,
vec![
clone_dir.join("README.md"),
clone_dir.join("test.plugin.zsh")
]
);
assert_eq!(locked.apply, vec![String::from("hello")]);
}
#[test]
fn external_plugin_lock_git_with_matches() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let ctx = create_test_context(dir);
let plugin = ExternalPlugin {
name: "test".to_string(),
source: Source::Git {
url: Url::parse("https://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: Some(GitReference::Tag("v0.1.0".to_string())),
},
dir: None,
uses: None,
apply: None,
};
let locked_source = plugin.source.clone().lock(&ctx).unwrap();
let clone_dir = dir.join("repos/github.com/rossmacarthur/sheldon-test");
let locked = plugin
.lock(
&ctx,
locked_source,
&["*.plugin.zsh".to_string()],
&["hello".to_string()],
)
.unwrap();
assert_eq!(locked.name, String::from("test"));
assert_eq!(locked.dir(), clone_dir);
assert_eq!(locked.files, vec![clone_dir.join("test.plugin.zsh")]);
assert_eq!(locked.apply, vec![String::from("hello")]);
}
#[test]
fn external_plugin_lock_remote() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let ctx = create_test_context(dir);
let plugin = ExternalPlugin {
name: "test".to_string(),
source: Source::Remote {
url: Url::parse(
"https://github.com/rossmacarthur/sheldon-test/raw/master/test.plugin.zsh",
)
.unwrap(),
},
dir: None,
uses: None,
apply: None,
};
let locked_source = plugin.source.clone().lock(&ctx).unwrap();
let download_dir = dir.join("downloads/github.com/rossmacarthur/sheldon-test/raw/master");
let locked = plugin
.lock(&ctx, locked_source, &[], &["hello".to_string()])
.unwrap();
assert_eq!(locked.name, String::from("test"));
assert_eq!(locked.dir(), download_dir);
assert_eq!(locked.files, vec![download_dir.join("test.plugin.zsh")]);
assert_eq!(locked.apply, vec![String::from("hello")]);
}
#[test]
fn config_lock_empty() {
let temp = tempfile::tempdir().expect("create temporary directory");
let dir = temp.path();
let ctx = create_test_context(dir);
let config = Config {
matches: None,
apply: None,
templates: IndexMap::new(),
plugins: Vec::new(),
};
let locked = config.lock(&ctx).unwrap();
assert_eq!(&locked.settings, ctx.settings());
assert_eq!(locked.plugins, Vec::new());
assert_eq!(locked.templates, IndexMap::new());
assert_eq!(locked.errors.len(), 0);
}
#[test]
fn config_lock_example_config() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let temp0 = tempfile::tempdir().expect("create temporary directory");
let local_dir = temp0.path();
let _ = git_clone_sheldon_test(&temp0);
let temp1 = tempfile::tempdir().expect("create temporary directory");
let root = temp1.path();
let config_file = manifest_dir.join("docs/plugins.example.toml");
let lock_file = root.join("plugins.lock");
let clone_dir = root.join("repos");
let download_dir = root.join("downloads");
let ctx = Context {
settings: Settings {
version: structopt::clap::crate_version!().to_string(),
root: root.to_path_buf(),
config_file,
lock_file,
clone_dir,
download_dir,
home: "/".into(),
},
output: crate::log::Output {
verbosity: crate::log::Verbosity::Quiet,
no_color: true,
},
reinstall: false,
};
let mut config = Config::from_path(ctx.config_file(), &mut Vec::new()).unwrap();
{
match &mut config.plugins[2] {
Plugin::External(ref mut plugin) => {
plugin.name = "sheldon-test".to_string();
plugin.source = Source::Local {
dir: local_dir.to_path_buf(),
};
}
_ => panic!("expected the 3rd plugin to be external"),
}
}
let locked = config.lock(&ctx).unwrap();
assert_eq!(locked.settings, ctx.settings);
assert_eq!(
locked.plugins,
vec![
LockedPlugin::External(LockedExternalPlugin {
name: "async".to_string(),
source_dir: root.join("repos/github.com/mafredri/zsh-async"),
plugin_dir: None,
files: vec![root.join("repos/github.com/mafredri/zsh-async/async.zsh")],
apply: vec_into!["function"]
}),
LockedPlugin::External(LockedExternalPlugin {
name: "pure".to_string(),
source_dir: root.join("repos/github.com/sindresorhus/pure"),
plugin_dir: None,
files: vec![root.join("repos/github.com/sindresorhus/pure/pure.zsh")],
apply: vec_into!["prompt"]
}),
LockedPlugin::External(LockedExternalPlugin {
name: "sheldon-test".to_string(),
source_dir: local_dir.to_path_buf(),
plugin_dir: None,
files: vec![root.join(local_dir.join("test.plugin.zsh"))],
apply: vec_into!["PATH", "source"]
}),
LockedPlugin::Inline(InlinePlugin {
name: "ip-netns".to_string(),
raw: r#"# Get ip netns information
ip_netns_prompt_info() {
if (( $+commands[ip] )); then
local ref="$(ip netns identify $$)"
if [[ ! -z "$ref" ]]; then
echo "${ZSH_THEME_IP_NETNS_PREFIX:=(}${ref}${ZSH_THEME_IP_NETNS_SUFFIX:=)}"
fi
fi
}
"#
.to_string()
}),
LockedPlugin::External(LockedExternalPlugin {
name: "docker-destroy-all".to_string(),
source_dir: root.join("repos/gist.github.com/79ee61f7c140c63d2786"),
plugin_dir: None,
files: vec![root
.join("repos/gist.github.com/79ee61f7c140c63d2786/get_last_pane_path.sh")],
apply: vec_into!["PATH"]
})
]
);
assert_eq!(
locked.templates,
indexmap_into![
"function" => Template {
value: "ln -sf \"{{ file }}\" \"{{ root }}/functions/{{ name }}\"".to_string(),
each: true
},
"prompt" => Template {
value:
"ln -sf \"{{ file }}\" \"{{ root }}/functions/prompt_{{ name }}_setup\"".to_string(),
each: true
}
]
);
assert_eq!(locked.errors.len(), 0);
}
#[test]
fn locked_config_clean() {
let temp = tempfile::tempdir().expect("create temporary directory");
let ctx = create_test_context(temp.path());
let config = Config {
matches: None,
apply: None,
templates: DEFAULT_TEMPLATES
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
plugins: vec![Plugin::External(ExternalPlugin {
name: "test".to_string(),
source: Source::Git {
url: Url::parse("git://github.com/rossmacarthur/sheldon-test").unwrap(),
reference: None,
},
dir: None,
uses: None,
apply: None,
})],
};
let locked = config.lock(&ctx).unwrap();
let test_dir = ctx.clone_dir().join("github.com/rossmacarthur/another-dir");
let test_file = test_dir.join("test.txt");
fs::create_dir_all(&test_dir).unwrap();
{
fs::OpenOptions::new()
.create(true)
.write(true)
.open(&test_file)
.unwrap();
}
let mut warnings = Vec::new();
locked.clean(&ctx, &mut warnings);
assert!(warnings.is_empty());
assert!(ctx
.clone_dir()
.join("github.com/rossmacarthur/sheldon-test")
.exists());
assert!(ctx
.clone_dir()
.join("github.com/rossmacarthur/sheldon-test/test.plugin.zsh")
.exists());
assert!(!test_file.exists());
assert!(!test_dir.exists());
}
#[test]
fn locked_config_to_and_from_path() {
let mut temp = tempfile::NamedTempFile::new().unwrap();
let content = r#"version = "<version>"
home = "<root>"
root = "<root>"
config_file = "<root>/plugins.toml"
lock_file = "<root>/plugins.lock"
clone_dir = "<root>/repos"
download_dir = "<root>/downloads"
plugins = []
[templates]
"#;
temp.write_all(content.as_bytes()).unwrap();
let locked_config = LockedConfig::from_path(temp.into_temp_path()).unwrap();
let temp = tempfile::NamedTempFile::new().unwrap();
let path = temp.into_temp_path();
locked_config.to_path(&path).unwrap();
assert_eq!(read_file_contents(&path).unwrap(), content);
}
}