use std::{
fs::{self, File},
io,
path::{Path, PathBuf},
process, time,
};
use anyhow::{Context as ResultExt, Error, Result};
use fs2::{lock_contended_error, FileExt};
use url::Url;
use crate::context::{Context, SettingsExt};
pub fn underlying_io_error_kind(error: &Error) -> Option<io::ErrorKind> {
for cause in error.chain() {
if let Some(io_error) = cause.downcast_ref::<io::Error>() {
return Some(io_error.kind());
}
}
None
}
fn nuke_path(path: &Path) -> io::Result<()> {
if path.is_dir() {
fs::remove_dir_all(path)
} else {
fs::remove_file(path)
}
}
pub fn download(url: Url) -> reqwest::Result<reqwest::blocking::Response> {
Ok(reqwest::blocking::get(url)?.error_for_status()?)
}
pub trait PathExt {
fn metadata_modified(&self) -> Option<time::SystemTime>;
fn newer_than<P>(&self, other: P) -> bool
where
P: AsRef<Path>;
fn expand_tilde<P>(&self, home: P) -> PathBuf
where
P: AsRef<Path>;
fn replace_home<P>(&self, home: P) -> PathBuf
where
P: AsRef<Path>;
}
impl PathExt for Path {
fn metadata_modified(&self) -> Option<time::SystemTime> {
fs::metadata(&self).and_then(|m| m.modified()).ok()
}
fn newer_than<P>(&self, other: P) -> bool
where
P: AsRef<Path>,
{
match (self.metadata_modified(), other.as_ref().metadata_modified()) {
(Some(self_time), Some(other_time)) => self_time > other_time,
_ => false,
}
}
fn expand_tilde<P>(&self, home: P) -> PathBuf
where
P: AsRef<Path>,
{
if let Ok(path) = self.strip_prefix("~") {
home.as_ref().join(path)
} else {
self.to_path_buf()
}
}
fn replace_home<P>(&self, home: P) -> PathBuf
where
P: AsRef<Path>,
{
if let Ok(path) = self.strip_prefix(home) {
Self::new("~").join(path)
} else {
self.to_path_buf()
}
}
}
pub struct TempPath {
pub path: Option<PathBuf>,
}
impl TempPath {
pub fn new(original_path: &Path) -> Self {
let mut path = original_path.parent().unwrap().to_path_buf();
let mut file_name = original_path.file_stem().unwrap().to_os_string();
file_name.push(format!("-tmp-{}", process::id()));
if let Some(ext) = original_path.extension() {
file_name.push(".");
file_name.push(ext);
}
path.push(file_name);
Self { path: Some(path) }
}
pub fn path(&self) -> &Path {
self.path.as_ref().unwrap()
}
pub fn write<R>(&mut self, mut reader: &mut R) -> io::Result<()>
where
R: io::Read,
{
let mut file = fs::File::create(self.path())?;
io::copy(&mut reader, &mut file)?;
Ok(())
}
pub fn rename(mut self, new_path: &Path) -> io::Result<()> {
if let Err(err) = nuke_path(new_path) {
if err.kind() != io::ErrorKind::NotFound {
return Err(err);
}
};
if let Some(path) = &self.path {
fs::rename(path, new_path)?;
self.path = None;
}
Ok(())
}
}
impl Drop for TempPath {
fn drop(&mut self) {
if let Some(path) = &self.path {
nuke_path(&path).ok();
}
}
}
#[derive(Debug)]
pub struct Mutex(File);
impl Mutex {
pub fn acquire(ctx: &Context, path: &Path) -> Result<Self> {
let file = fs::OpenOptions::new()
.read(true)
.open(path)
.with_context(s!("failed to open `{}`", path.display()))?;
if let Err(e) = file.try_lock_exclusive() {
let msg = s!("failed to acquire file lock `{}`", path.display());
if e.raw_os_error() == lock_contended_error().raw_os_error() {
warning!(
ctx,
"Blocking",
&format!(
"waiting for file lock on {}",
ctx.replace_home(path).display()
)
);
file.lock_exclusive().with_context(msg)?;
} else {
return Err(e).with_context(msg);
}
}
Ok(Self(file))
}
}
impl Drop for Mutex {
fn drop(&mut self) {
self.0.unlock().ok();
}
}
pub mod git {
use std::path::Path;
use git2::{
build::RepoBuilder, BranchType, Cred, CredentialType, Error, FetchOptions, Oid,
RemoteCallbacks, Repository, ResetType,
};
use url::Url;
use anyhow::Context as ResultExt;
fn with_fetch_options<T, F>(f: F) -> anyhow::Result<T>
where
F: FnOnce(FetchOptions<'_>) -> anyhow::Result<T>,
{
let mut rcb = RemoteCallbacks::new();
rcb.credentials(|_, username, allowed| {
if allowed.contains(CredentialType::SSH_KEY) {
if let Some(username) = username {
return Cred::ssh_key_from_agent(username);
}
}
if allowed.contains(CredentialType::DEFAULT) {
return Cred::default();
}
Err(Error::from_str(
"remote authentication required but none available",
))
});
let mut opts = FetchOptions::new();
opts.remote_callbacks(rcb);
f(opts)
}
pub fn open(dir: &Path) -> anyhow::Result<Repository> {
let repo = Repository::open(dir)
.with_context(s!("failed to open repository at `{}`", dir.display()))?;
Ok(repo)
}
pub fn clone(url: &Url, dir: &Path) -> anyhow::Result<Repository> {
with_fetch_options(|opts| {
let repo = RepoBuilder::new()
.fetch_options(opts)
.clone(url.as_str(), dir)
.with_context(s!("failed to git clone `{}`", url))?;
Ok(repo)
})
}
pub fn checkout(repo: &Repository, oid: Oid) -> anyhow::Result<()> {
let obj = repo
.find_object(oid, None)
.with_context(s!("failed to find `{}`", oid))?;
repo.reset(&obj, ResetType::Hard, None)
.with_context(s!("failed to set HEAD to `{}`", oid))?;
repo.checkout_tree(&obj, None)
.with_context(s!("failed to checkout `{}`", oid))
}
pub fn submodule_update(repo: &Repository) -> Result<(), Error> {
fn _submodule_update(repo: &Repository, todo: &mut Vec<Repository>) -> Result<(), Error> {
for mut submodule in repo.submodules()? {
submodule.update(true, None)?;
todo.push(submodule.open()?);
}
Ok(())
}
let mut repos = Vec::new();
_submodule_update(&repo, &mut repos)?;
while let Some(repo) = repos.pop() {
_submodule_update(&repo, &mut repos)?;
}
Ok(())
}
pub fn resolve_branch(repo: &Repository, branch: &str) -> anyhow::Result<Oid> {
repo.find_branch(&format!("origin/{}", branch), BranchType::Remote)
.with_context(s!("failed to find branch `{}`", branch))?
.get()
.target()
.with_context(s!("branch `{}` does not have a target", branch))
}
pub fn resolve_rev(repo: &Repository, rev: &str) -> anyhow::Result<Oid> {
let obj = repo
.revparse_single(rev)
.with_context(s!("failed to find revision `{}`", rev))?;
Ok(match obj.as_tag() {
Some(tag) => tag.target_id(),
None => obj.id(),
})
}
pub fn resolve_tag(repo: &Repository, tag: &str) -> anyhow::Result<Oid> {
fn _resolve_tag(repo: &Repository, tag: &str) -> Result<Oid, Error> {
let id = repo.refname_to_id(&format!("refs/tags/{}", tag))?;
let obj = repo.find_object(id, None)?;
let obj = obj.peel(git2::ObjectType::Commit)?;
Ok(obj.id())
}
_resolve_tag(repo, tag).with_context(s!("failed to find tag `{}`", tag))
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn path_buf_expand_tilde_with_root() {
assert_eq!(PathBuf::from("/").expand_tilde("/test"), PathBuf::from("/"))
}
#[test]
fn path_buf_expand_tilde_with_folder_in_root() {
assert_eq!(
PathBuf::from("/fol/der").expand_tilde("/test"),
PathBuf::from("/fol/der")
)
}
#[test]
fn path_buf_expand_tilde_with_home() {
assert_eq!(
PathBuf::from("~/").expand_tilde("/test"),
PathBuf::from("/test")
)
}
#[test]
fn path_buf_expand_tilde_with_folder_in_home() {
assert_eq!(
PathBuf::from("~/fol/der").expand_tilde("/test"),
PathBuf::from("/test/fol/der")
)
}
#[test]
fn path_buf_replace_home_with_root() {
assert_eq!(
PathBuf::from("/not/home").replace_home("/test/home"),
PathBuf::from("/not/home")
)
}
#[test]
fn path_buf_replace_home_with_home() {
assert_eq!(
PathBuf::from("/test/home").replace_home("/test/home"),
PathBuf::from("~")
)
}
}