Setup apply to sync between desktop and homesync repo.

Also remove unused commands.
pull/3/head
Joshua Potter 2022-01-02 13:48:18 -05:00
parent 61b9a338a5
commit bf65142e61
8 changed files with 211 additions and 115 deletions

View File

@ -14,7 +14,6 @@ clap = { version = "3.0.0-rc.9", features = ["derive"] }
git2 = "0.13.25" git2 = "0.13.25"
log = "0.4.14" log = "0.4.14"
notify = "4.0.16" notify = "4.0.16"
regex = "1.5.4"
serde = "1.0" serde = "1.0"
serde_derive = "1.0.132" serde_derive = "1.0.132"
serde_yaml = "0.8" serde_yaml = "0.8"

View File

@ -19,7 +19,11 @@
rustc rustc
rustfmt rustfmt
] ++ lib.optionals stdenv.isDarwin ( ] ++ lib.optionals stdenv.isDarwin (
with darwin.apple_sdk.frameworks; [ CoreServices ]); with darwin.apple_sdk.frameworks; [ CoreServices ]
) ++ lib.optionals stdenv.isLinux [
pkgs.openssl
pkgs.zlib
];
}; };
}); });
} }

View File

@ -96,17 +96,23 @@ impl Config {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct PathConfig(pub ResPathBuf, pub Config); pub struct PathConfig {
pub homesync_yml: ResPathBuf,
pub config: Config,
}
impl PathConfig { impl PathConfig {
pub fn new(path: &ResPathBuf, config: Config) -> Self { pub fn new(path: &ResPathBuf, config: Config) -> Self {
PathConfig(path.clone(), config) PathConfig {
homesync_yml: path.clone(),
config,
}
} }
// TODO(jrpotter): Create backup file before overwriting. // TODO(jrpotter): Create backup file before overwriting.
pub fn write(&self) -> Result<()> { pub fn write(&self) -> Result<()> {
let mut file = fs::File::create(&self.0)?; let mut file = fs::File::create(&self.homesync_yml)?;
let serialized = serde_yaml::to_string(&self.1)?; let serialized = serde_yaml::to_string(&self.config)?;
file.write_all(serialized.as_bytes())?; file.write_all(serialized.as_bytes())?;
Ok(()) Ok(())
} }
@ -143,12 +149,12 @@ pub fn load(candidates: &Vec<ResPathBuf>) -> Result<PathConfig> {
Err(Error::MissingConfig) Err(Error::MissingConfig)
} }
pub fn reload(config: &PathConfig) -> Result<PathConfig> { pub fn reload(pc: &PathConfig) -> Result<PathConfig> {
info!( info!(
"<green>{}</> configuration reloaded.", "<green>{}</> configuration reloaded.",
config.1.local.display() pc.config.local.display()
); );
load(&vec![config.0.clone()]) load(&vec![pc.homesync_yml.clone()])
} }
// ======================================== // ========================================
@ -205,14 +211,14 @@ pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
Some(c) => Some(&c.remote), Some(c) => Some(&c.remote),
None => None, None => None,
})?; })?;
let generated = PathConfig( let generated = PathConfig {
path.clone(), homesync_yml: path.clone(),
Config { config: Config {
local, local,
remote, remote,
packages: loaded.map_or(BTreeMap::new(), |c| c.packages), packages: loaded.map_or(BTreeMap::new(), |c| c.packages),
}, },
); };
generated.write()?; generated.write()?;
Ok(generated) Ok(generated)
} }
@ -221,13 +227,16 @@ pub fn write(path: &ResPathBuf, loaded: Option<Config>) -> Result<PathConfig> {
// Listing // Listing
// ======================================== // ========================================
pub fn list_packages(config: PathConfig) { pub fn list_packages(pc: PathConfig) {
println!( println!(
"Listing packages in {}...\n", "Listing packages in {}...\n",
colorize_string(format!("<green>{}</>", config.0.unresolved().display())), colorize_string(format!(
"<green>{}</>",
pc.homesync_yml.unresolved().display()
)),
); );
// Alphabetical ordered ensured by B-tree implementation. // Alphabetical ordered ensured by B-tree implementation.
for (k, _) in config.1.packages { for (k, _) in pc.config.packages {
println!("{}", k); println!("{}", k);
} }
} }

View File

@ -107,7 +107,7 @@ impl<'a> WatchState<'a> {
/// Reads in the new path config, updating all watched and pending files /// Reads in the new path config, updating all watched and pending files
/// according to the packages in the specified config. /// according to the packages in the specified config.
pub fn update(&mut self, config: &PathConfig) { pub fn update(&mut self, pc: &PathConfig) {
self.send_poll(PollEvent::Clear); self.send_poll(PollEvent::Clear);
for path in &self.watching { for path in &self.watching {
match self.watcher.unwatch(&path) { match self.watcher.unwatch(&path) {
@ -122,7 +122,7 @@ impl<'a> WatchState<'a> {
} }
} }
self.watching.clear(); self.watching.clear();
for (_, package) in &config.1.packages { for (_, package) in &pc.config.packages {
for path in &package.configs { for path in &package.configs {
match path::soft_resolve(&path) { match path::soft_resolve(&path) {
Ok(None) => self.send_poll(PollEvent::Pending(path.clone())), Ok(None) => self.send_poll(PollEvent::Pending(path.clone())),
@ -138,7 +138,7 @@ impl<'a> WatchState<'a> {
// Daemon // Daemon
// ======================================== // ========================================
pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> { pub fn launch(mut pc: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> {
let (poll_tx, poll_rx) = channel(); let (poll_tx, poll_rx) = channel();
let (watch_tx, watch_rx) = channel(); let (watch_tx, watch_rx) = channel();
let watch_tx1 = watch_tx.clone(); let watch_tx1 = watch_tx.clone();
@ -152,9 +152,9 @@ pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
// changes to it for hot reloading purposes, and not worry that our wrapper // changes to it for hot reloading purposes, and not worry that our wrapper
// will ever clear it from its watch state. // will ever clear it from its watch state.
let mut watcher: RecommendedWatcher = Watcher::new(watch_tx1, Duration::from_secs(freq_secs))?; let mut watcher: RecommendedWatcher = Watcher::new(watch_tx1, Duration::from_secs(freq_secs))?;
watcher.watch(&config.0, RecursiveMode::NonRecursive)?; watcher.watch(&pc.homesync_yml, RecursiveMode::NonRecursive)?;
let mut state = WatchState::new(poll_tx, &mut watcher)?; let mut state = WatchState::new(poll_tx, &mut watcher)?;
state.update(&config); state.update(&pc);
loop { loop {
// Received paths should always be the fully resolved ones so safe to // Received paths should always be the fully resolved ones so safe to
// compare against our current config path. // compare against our current config path.
@ -166,16 +166,16 @@ pub fn launch(mut config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Erro
trace!("NoticeRemove {}", p.display()); trace!("NoticeRemove {}", p.display());
} }
Ok(DebouncedEvent::Create(p)) => { Ok(DebouncedEvent::Create(p)) => {
if config.0 == p { if pc.homesync_yml == p {
config = config::reload(&config)?; pc = config::reload(&pc)?;
state.update(&config); state.update(&pc);
} }
trace!("Create {}", p.display()); trace!("Create {}", p.display());
} }
Ok(DebouncedEvent::Write(p)) => { Ok(DebouncedEvent::Write(p)) => {
if config.0 == p { if pc.homesync_yml == p {
config = config::reload(&config)?; pc = config::reload(&pc)?;
state.update(&config); state.update(&pc);
} }
trace!("Write {}", p.display()); trace!("Write {}", p.display());
} }

View File

@ -1,7 +1,43 @@
use super::{config::PathConfig, path}; use super::{config::PathConfig, path};
use git2::Repository; use git2::Repository;
use path::ResPathBuf;
use simplelog::{info, paris}; use simplelog::{info, paris};
use std::{env::VarError, error, fmt, fs, io, path::PathBuf, result}; use std::{
collections::HashSet,
env::VarError,
error, fmt, fs, io,
path::{Path, PathBuf},
result,
};
// All git error codes.
// TODO(jrpotter): Remove these once done needing to reference them.
// git2::ErrorCode::GenericError => panic!("generic"),
// git2::ErrorCode::NotFound => panic!("not_found"),
// git2::ErrorCode::Exists => panic!("exists"),
// git2::ErrorCode::Ambiguous => panic!("ambiguous"),
// git2::ErrorCode::BufSize => panic!("buf_size"),
// git2::ErrorCode::User => panic!("user"),
// git2::ErrorCode::BareRepo => panic!("bare_repo"),
// git2::ErrorCode::UnbornBranch => panic!("unborn_branch"),
// git2::ErrorCode::Unmerged => panic!("unmerged"),
// git2::ErrorCode::NotFastForward => panic!("not_fast_forward"),
// git2::ErrorCode::InvalidSpec => panic!("invalid_spec"),
// git2::ErrorCode::Conflict => panic!("conflict"),
// git2::ErrorCode::Locked => panic!("locked"),
// git2::ErrorCode::Modified => panic!("modified"),
// git2::ErrorCode::Auth => panic!("auth"),
// git2::ErrorCode::Certificate => panic!("certificate"),
// git2::ErrorCode::Applied => panic!("applied"),
// git2::ErrorCode::Peel => panic!("peel"),
// git2::ErrorCode::Eof => panic!("eof"),
// git2::ErrorCode::Invalid => panic!("invalid"),
// git2::ErrorCode::Uncommitted => panic!("uncommitted"),
// git2::ErrorCode::Directory => panic!("directory"),
// git2::ErrorCode::MergeConflict => panic!("merge_conflict"),
// git2::ErrorCode::HashsumMismatch => panic!("hashsum_mismatch"),
// git2::ErrorCode::IndexDirty => panic!("index_dirty"),
// git2::ErrorCode::ApplyFail => panic!("apply_fail"),
// ======================================== // ========================================
// Error // Error
@ -14,6 +50,7 @@ pub enum Error {
GitError(git2::Error), GitError(git2::Error),
IOError(io::Error), IOError(io::Error),
NotHomesyncRepo, NotHomesyncRepo,
NotWorkingRepo,
VarError(VarError), VarError(VarError),
} }
@ -53,6 +90,10 @@ impl fmt::Display for Error {
f, f,
"Local repository is not managed by `homesync`. Missing `.homesync` sentinel file." "Local repository is not managed by `homesync`. Missing `.homesync` sentinel file."
), ),
Error::NotWorkingRepo => write!(
f,
"Local repository should be a working directory. Did you manually initialize with `--bare`?"
),
Error::VarError(e) => write!(f, "{}", e), Error::VarError(e) => write!(f, "{}", e),
} }
} }
@ -64,53 +105,17 @@ impl error::Error for Error {}
// Initialization // Initialization
// ======================================== // ========================================
// All git error codes.
// TODO(jrpotter): Remove these once done needing to reference them.
// git2::ErrorCode::GenericError => panic!("generic"),
// git2::ErrorCode::NotFound => panic!("not_found"),
// git2::ErrorCode::Exists => panic!("exists"),
// git2::ErrorCode::Ambiguous => panic!("ambiguous"),
// git2::ErrorCode::BufSize => panic!("buf_size"),
// git2::ErrorCode::User => panic!("user"),
// git2::ErrorCode::BareRepo => panic!("bare_repo"),
// git2::ErrorCode::UnbornBranch => panic!("unborn_branch"),
// git2::ErrorCode::Unmerged => panic!("unmerged"),
// git2::ErrorCode::NotFastForward => panic!("not_fast_forward"),
// git2::ErrorCode::InvalidSpec => panic!("invalid_spec"),
// git2::ErrorCode::Conflict => panic!("conflict"),
// git2::ErrorCode::Locked => panic!("locked"),
// git2::ErrorCode::Modified => panic!("modified"),
// git2::ErrorCode::Auth => panic!("auth"),
// git2::ErrorCode::Certificate => panic!("certificate"),
// git2::ErrorCode::Applied => panic!("applied"),
// git2::ErrorCode::Peel => panic!("peel"),
// git2::ErrorCode::Eof => panic!("eof"),
// git2::ErrorCode::Invalid => panic!("invalid"),
// git2::ErrorCode::Uncommitted => panic!("uncommitted"),
// git2::ErrorCode::Directory => panic!("directory"),
// git2::ErrorCode::MergeConflict => panic!("merge_conflict"),
// git2::ErrorCode::HashsumMismatch => panic!("hashsum_mismatch"),
// git2::ErrorCode::IndexDirty => panic!("index_dirty"),
// git2::ErrorCode::ApplyFail => panic!("apply_fail"),
/// Sets up a local github repository all configuration files will be synced to. /// Sets up a local github repository all configuration files will be synced to.
/// If there does not exist a local repository at the requested location, we /// If there does not exist a local repository at the requested location, we
/// attempt to make it. /// attempt to make it.
/// ///
/// NOTE! This does not perform any syncing between local and remote. In fact, /// NOTE! This does not perform any syncing between local and remote. In fact,
/// this method does not perform any validation on the remote. /// this method does not perform any validation on remote at all.
pub fn init(config: &PathConfig) -> Result<git2::Repository> { pub fn init(pc: &PathConfig) -> Result<Repository> {
// Permit the use of environment variables within the local configuration // Permit the use of environment variables within the local configuration
// path (e.g. `$HOME`). Unlike with resolution, we want to fail if the // path (e.g. `$HOME`). Unlike with resolution, we want to fail if the
// environment variable is not defined. // environment variable is not defined.
let expanded = match config.1.local.to_str() { let expanded = path::expand(&pc.config.local)?;
Some(s) => s,
None => Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Could not local path to a UTF-8 encoded string.",
))?,
};
let expanded = path::expand_env(expanded)?;
// Attempt to open the local path as a git repository if possible. The // Attempt to open the local path as a git repository if possible. The
// `NotFound` error is thrown if: // `NotFound` error is thrown if:
// //
@ -146,7 +151,7 @@ pub fn init(config: &PathConfig) -> Result<git2::Repository> {
None => { None => {
info!( info!(
"Creating new homesync repository at <green>{}</>.", "Creating new homesync repository at <green>{}</>.",
config.1.local.display() pc.config.local.display()
); );
let repo = Repository::init(&expanded)?; let repo = Repository::init(&expanded)?;
fs::File::create(sentinel)?; fs::File::create(sentinel)?;
@ -154,3 +159,76 @@ pub fn init(config: &PathConfig) -> Result<git2::Repository> {
} }
} }
} }
// ========================================
// Application
// ========================================
fn find_repo_files(path: &Path) -> Result<Vec<ResPathBuf>> {
let mut seen = Vec::new();
if path.is_dir() {
for entry in fs::read_dir(path)? {
let nested = entry?.path();
if nested.is_dir() {
if nested.ends_with(".git") {
continue;
}
let nested = find_repo_files(&nested)?;
seen.extend_from_slice(&nested);
} else if !nested.ends_with(".homesync") {
seen.push(ResPathBuf::new(&nested)?);
}
}
}
Ok(seen)
}
fn find_package_files(pc: &PathConfig) -> Vec<ResPathBuf> {
let mut seen = Vec::new();
for (_, package) in &pc.config.packages {
for path in &package.configs {
if let Ok(resolved) = path::resolve(path) {
seen.push(resolved);
}
}
}
seen
}
pub fn apply(pc: &PathConfig, repo: &Repository) -> Result<()> {
let workdir = repo.workdir().ok_or(Error::NotWorkingRepo)?;
let repo_files = find_repo_files(&workdir)?;
let package_files = find_package_files(&pc);
// Find all files in our repository that are no longer being referenced in
// our primary config file. They should be removed from the repository.
let lookup_files: HashSet<PathBuf> = package_files
.iter()
.map(|m| m.unresolved().to_path_buf())
.collect();
for repo_file in &repo_files {
let relative = repo_file
.resolved()
.strip_prefix(workdir)
.expect("Relative git file could not be stripped properly.")
.to_path_buf();
if !lookup_files.contains(&relative) {
fs::remove_file(repo_file)?;
}
if let Some(p) = repo_file.resolved().parent() {
if p.read_dir()?.next().is_none() {
fs::remove_dir(p)?;
}
}
}
// Find all resolvable files in our primary config and copy them into the
// repository.
for package_file in &package_files {
let mut copy = workdir.to_path_buf();
copy.push(package_file.unresolved());
if let Some(p) = copy.parent() {
fs::create_dir_all(p)?;
}
fs::copy(package_file.resolved(), copy)?;
}
Ok(())
}

View File

@ -7,17 +7,20 @@ use config::PathConfig;
use path::ResPathBuf; use path::ResPathBuf;
use std::{error::Error, io}; use std::{error::Error, io};
pub fn run_add(_config: PathConfig) -> Result<(), config::Error> { type Result = std::result::Result<(), Box<dyn Error>>;
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
pub fn run_apply(config: PathConfig) -> Result {
let repo = git::init(&config)?;
git::apply(&config, &repo)?;
Ok(()) Ok(())
} }
pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result<(), Box<dyn Error>> { pub fn run_daemon(config: PathConfig, freq_secs: u64) -> Result {
daemon::launch(config, freq_secs)?; daemon::launch(config, freq_secs)?;
Ok(()) Ok(())
} }
pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> { pub fn run_init(candidates: Vec<ResPathBuf>) -> Result {
debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`."); debug_assert!(!candidates.is_empty(), "Empty candidates found in `init`.");
if candidates.is_empty() { if candidates.is_empty() {
Err(config::Error::IOError(io::Error::new( Err(config::Error::IOError(io::Error::new(
@ -29,7 +32,7 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
// Check if we already have a local config somewhere. If so, reprompt // Check if we already have a local config somewhere. If so, reprompt
// the same configuration options and override the values present in the // the same configuration options and override the values present in the
// current YAML file. // current YAML file.
Ok(loaded) => config::write(&loaded.0, Some(loaded.1))?, Ok(loaded) => config::write(&loaded.homesync_yml, Some(loaded.config))?,
// Otherwise create a new config file at the given location. We always // Otherwise create a new config file at the given location. We always
// assume we want to write to the first file in our priority list. If // assume we want to write to the first file in our priority list. If
// not, the user should specify which config they want to write using // not, the user should specify which config they want to write using
@ -41,23 +44,12 @@ pub fn run_init(candidates: Vec<ResPathBuf>) -> Result<(), Box<dyn Error>> {
} }
Err(e) => Err(e)?, Err(e) => Err(e)?,
}; };
// Verify (or create) our local and remote git repositories. The internal
// git library we chose to use employs async/await so let's wrap around a
// channel.
git::init(&config)?; git::init(&config)?;
println!("\nFinished initialization."); println!("\nFinished initialization.");
Ok(()) Ok(())
} }
pub fn run_list(config: PathConfig) -> Result<(), config::Error> { pub fn run_list(config: PathConfig) -> Result {
config::list_packages(config); config::list_packages(config);
Ok(()) Ok(())
} }
pub fn run_pull(_config: PathConfig) -> Result<(), Box<dyn Error>> {
Ok(())
}
pub fn run_push(_config: PathConfig) -> Result<(), Box<dyn Error>> {
Ok(())
}

View File

@ -16,7 +16,7 @@ fn main() {
.expect("Could not initialize logger library."); .expect("Could not initialize logger library.");
let matches = App::new("homesync") let matches = App::new("homesync")
.about("Cross desktop configuration sync tool.") .about("Cross desktop sync tool.")
.version("0.1.0") .version("0.1.0")
.setting(AppSettings::SubcommandRequiredElseHelp) .setting(AppSettings::SubcommandRequiredElseHelp)
.author("Joshua Potter <jrpotter.github.io>") .author("Joshua Potter <jrpotter.github.io>")
@ -28,7 +28,9 @@ fn main() {
.help("Specify a configuration file to use in place of defaults") .help("Specify a configuration file to use in place of defaults")
.takes_value(true), .takes_value(true),
) )
.subcommand(App::new("add").about("Add new configuration to local repository")) .subcommand(
App::new("apply").about("Find all changes and apply them to the local repository"),
)
.subcommand( .subcommand(
App::new("daemon") App::new("daemon")
.about("Start up a new homesync daemon") .about("Start up a new homesync daemon")
@ -50,8 +52,6 @@ fn main() {
) )
.subcommand(App::new("init").about("Initialize the homesync local repository")) .subcommand(App::new("init").about("Initialize the homesync local repository"))
.subcommand(App::new("list").about("See which packages homesync manages")) .subcommand(App::new("list").about("See which packages homesync manages"))
.subcommand(App::new("pull").about("Pull remote repository into local repository"))
.subcommand(App::new("push").about("Push local repository to remote repository"))
.get_matches(); .get_matches();
if let Err(e) = dispatch(matches) { if let Err(e) = dispatch(matches) {
@ -71,7 +71,7 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
subcommand => { subcommand => {
let config = homesync::config::load(&candidates)?; let config = homesync::config::load(&candidates)?;
match subcommand { match subcommand {
Some(("add", _)) => Ok(homesync::run_add(config)?), Some(("apply", _)) => Ok(homesync::run_apply(config)?),
Some(("daemon", matches)) => { Some(("daemon", matches)) => {
let freq_secs: u64 = match matches.value_of("frequency") { let freq_secs: u64 = match matches.value_of("frequency") {
Some(f) => f.parse().unwrap_or(0), Some(f) => f.parse().unwrap_or(0),
@ -85,8 +85,6 @@ fn dispatch(matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
Some(("list", _)) => Ok(homesync::run_list(config)?), Some(("list", _)) => Ok(homesync::run_list(config)?),
Some(("pull", _)) => Ok(homesync::run_pull(config)?),
Some(("push", _)) => Ok(homesync::run_push(config)?),
_ => unreachable!(), _ => unreachable!(),
} }
} }

View File

@ -1,4 +1,3 @@
use regex::Regex;
use serde::{ use serde::{
de, de,
de::{Unexpected, Visitor}, de::{Unexpected, Visitor},
@ -61,13 +60,30 @@ pub struct ResPathBuf {
unresolved: PathBuf, unresolved: PathBuf,
} }
fn unresolved_error(path: &Path) -> io::Error {
io::Error::new(
io::ErrorKind::Other,
format!("Path '{}' should be fully resolved.", path.display()),
)
}
impl ResPathBuf { impl ResPathBuf {
pub fn display(&self) -> std::path::Display { pub fn new(path: &Path) -> Result<Self> {
self.inner.display() if !path.is_absolute() {
Err(unresolved_error(path))?;
}
Ok(ResPathBuf {
inner: path.to_path_buf(),
unresolved: path.to_path_buf(),
})
}
pub fn resolved(&self) -> &PathBuf {
&self.inner
} }
pub fn unresolved(&self) -> &PathBuf { pub fn unresolved(&self) -> &PathBuf {
return &self.unresolved; &self.unresolved
} }
} }
@ -214,20 +230,8 @@ pub fn validate_is_dir(path: &Path) -> Result<()> {
/// Find environment variables within the argument and expand them if possible. /// Find environment variables within the argument and expand them if possible.
/// ///
/// Returns an error if any found environment variables are not defined. /// Returns an error if any found environment variables are not defined.
pub fn expand_env(s: &str) -> Result<String> { pub fn expand(path: &Path) -> Result<PathBuf> {
let re = Regex::new(r"\$(?P<env>[[:alnum:]]+)").unwrap(); let mut expanded = env::current_dir()?;
let mut path = s.to_owned();
for caps in re.captures_iter(s) {
let evar = env::var(&caps["env"])?;
path = path.replace(&format!("${}", &caps["env"]), &evar);
}
Ok(path)
}
/// Attempt to resolve the provided path, returning a fully resolved path
/// instance if successful.
pub fn resolve(path: &Path) -> Result<ResPathBuf> {
let mut resolved = env::current_dir()?;
for comp in path.components() { for comp in path.components() {
match comp { match comp {
Component::Prefix(_) => Err(io::Error::new( Component::Prefix(_) => Err(io::Error::new(
@ -235,12 +239,12 @@ pub fn resolve(path: &Path) -> Result<ResPathBuf> {
"We do not currently support Windows.", "We do not currently support Windows.",
))?, ))?,
Component::RootDir => { Component::RootDir => {
resolved.clear(); expanded.clear();
resolved.push(Component::RootDir) expanded.push(Component::RootDir)
} }
Component::CurDir => (), Component::CurDir => (),
Component::ParentDir => { Component::ParentDir => {
if !resolved.pop() { if !expanded.pop() {
Err(io::Error::new( Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"Cannot take parent of root.", "Cannot take parent of root.",
@ -248,11 +252,23 @@ pub fn resolve(path: &Path) -> Result<ResPathBuf> {
} }
} }
Component::Normal(c) => { Component::Normal(c) => {
let c: OsString = expand_env(&c.to_string_lossy())?.into(); let lossy = c.to_string_lossy();
resolved.push(Component::Normal(&c)); if lossy.starts_with("$") {
let evar = env::var(lossy.replacen("$", "", 1))?;
expanded.push(Component::Normal(&OsString::from(evar)));
} else {
expanded.push(c);
} }
} }
} }
}
Ok(expanded)
}
/// Attempt to resolve the provided path, returning a fully resolved path
/// instance if successful.
pub fn resolve(path: &Path) -> Result<ResPathBuf> {
let resolved = expand(&path)?;
let resolved = resolved.canonicalize()?; let resolved = resolved.canonicalize()?;
Ok(ResPathBuf { Ok(ResPathBuf {
inner: resolved, inner: resolved,