refactor: run git through the same layer as everything else
This commit is contained in:
156
src/cmd/git.rs
Normal file
156
src/cmd/git.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::{Argv, Cmd};
|
||||||
|
|
||||||
|
// -C has no long form; it runs git as though from that directory
|
||||||
|
fn git(root: &Path) -> Argv {
|
||||||
|
Argv::new("git").arg("-C").arg(root.to_string_lossy())
|
||||||
|
}
|
||||||
|
|
||||||
|
// the root of the repository the working directory is in
|
||||||
|
pub struct RevParse;
|
||||||
|
|
||||||
|
impl Cmd for RevParse {
|
||||||
|
fn argv(&self) -> Argv {
|
||||||
|
Argv::new("git").arg("rev-parse").arg("--show-toplevel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the url a remote points at, if the repository has one
|
||||||
|
pub struct ConfigGet<'a> {
|
||||||
|
pub key: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for ConfigGet<'_> {
|
||||||
|
fn argv(&self) -> Argv {
|
||||||
|
Argv::new("git").arg("config").flag("--get", self.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LsFiles<'a> {
|
||||||
|
root: &'a Path,
|
||||||
|
tracked: bool,
|
||||||
|
ignored: bool,
|
||||||
|
pathspecs: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LsFiles<'a> {
|
||||||
|
// paths git would not restore: untracked ones, and whole directories rather
|
||||||
|
// than every file inside them
|
||||||
|
pub fn untracked(root: &'a Path) -> Self {
|
||||||
|
Self {
|
||||||
|
root,
|
||||||
|
tracked: false,
|
||||||
|
ignored: false,
|
||||||
|
pathspecs: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tracked(root: &'a Path) -> Self {
|
||||||
|
Self {
|
||||||
|
root,
|
||||||
|
tracked: true,
|
||||||
|
ignored: false,
|
||||||
|
pathspecs: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the ignored paths instead of the merely untracked ones
|
||||||
|
pub fn ignored(mut self) -> Self {
|
||||||
|
self.ignored = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn limited_to<P: AsRef<Path>>(mut self, pathspecs: &[P]) -> Self {
|
||||||
|
self.pathspecs = pathspecs
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.as_ref().to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for LsFiles<'_> {
|
||||||
|
fn argv(&self) -> Argv {
|
||||||
|
let mut argv = git(self.root).arg("ls-files");
|
||||||
|
|
||||||
|
if self.tracked {
|
||||||
|
argv = argv.arg("--cached");
|
||||||
|
} else {
|
||||||
|
// -z has no long form; it separates the paths with NUL, which is the
|
||||||
|
// only separator a filename cannot contain
|
||||||
|
argv = argv
|
||||||
|
.arg("-z")
|
||||||
|
.arg("--others")
|
||||||
|
.arg("--exclude-standard")
|
||||||
|
.arg("--directory")
|
||||||
|
.arg("--no-empty-directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ignored {
|
||||||
|
argv = argv.arg("--ignored");
|
||||||
|
}
|
||||||
|
|
||||||
|
argv.arg("--").args(&self.pathspecs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// whether a path is ignored, said through the exit code alone
|
||||||
|
pub struct CheckIgnore<'a> {
|
||||||
|
root: &'a Path,
|
||||||
|
path: &'a Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CheckIgnore<'a> {
|
||||||
|
pub fn new(root: &'a Path, path: &'a Path) -> Self {
|
||||||
|
Self { root, path }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cmd for CheckIgnore<'_> {
|
||||||
|
fn argv(&self) -> Argv {
|
||||||
|
git(self.root)
|
||||||
|
.arg("check-ignore")
|
||||||
|
.arg("--quiet")
|
||||||
|
.arg("--")
|
||||||
|
.arg(self.path.to_string_lossy())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use super::{CheckIgnore, LsFiles};
|
||||||
|
use crate::cmd::Cmd;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_untracked_listing_asks_for_whole_directories() {
|
||||||
|
let argv = LsFiles::untracked(Path::new("/repo"))
|
||||||
|
.ignored()
|
||||||
|
.limited_to(&[PathBuf::from("a b")])
|
||||||
|
.argv();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
argv.quoted(),
|
||||||
|
"git -C /repo ls-files -z --others --exclude-standard --directory \
|
||||||
|
--no-empty-directory --ignored -- 'a b'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_tracked_listing_asks_git_only_about_the_paths_given() {
|
||||||
|
let argv = LsFiles::tracked(Path::new("/repo"))
|
||||||
|
.limited_to(&[PathBuf::from(".env")])
|
||||||
|
.argv();
|
||||||
|
|
||||||
|
assert_eq!(argv.quoted(), "git -C /repo ls-files --cached -- .env");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_ignore_says_nothing_and_reports_through_its_status() {
|
||||||
|
let argv = CheckIgnore::new(Path::new("/repo"), Path::new(".env")).argv();
|
||||||
|
|
||||||
|
assert_eq!(argv.quoted(), "git -C /repo check-ignore --quiet -- .env");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ mod argv;
|
|||||||
mod compose;
|
mod compose;
|
||||||
mod django;
|
mod django;
|
||||||
mod docker;
|
mod docker;
|
||||||
|
mod git;
|
||||||
mod postgres;
|
mod postgres;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ pub use argv::Argv;
|
|||||||
pub use compose::{Config, Ps, Run, Start, Stop, Up};
|
pub use compose::{Config, Ps, Run, Start, Stop, Up};
|
||||||
pub use django::{Bash, Manage, Words};
|
pub use django::{Bash, Manage, Words};
|
||||||
pub use docker::{Cp, Exec};
|
pub use docker::{Cp, Exec};
|
||||||
|
pub use git::{CheckIgnore, ConfigGet, LsFiles, RevParse};
|
||||||
pub use postgres::{CreateDb, DropDb, PgDump, PgDumpAll, PgIsReady, PgRestore, Psql};
|
pub use postgres::{CreateDb, DropDb, PgDump, PgDumpAll, PgIsReady, PgRestore, Psql};
|
||||||
pub use shell::{Gunzip, Gzip, Head, Pipeline, Rm};
|
pub use shell::{Gunzip, Gzip, Head, Pipeline, Rm};
|
||||||
|
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -67,9 +67,9 @@ fn run(ctx: &Ctx, command: cli::Commands) -> Result<ExitCode> {
|
|||||||
paths,
|
paths,
|
||||||
force,
|
force,
|
||||||
store,
|
store,
|
||||||
} => scripts::link::add(&paths, force, store.root.as_deref()).map(|()| done),
|
} => scripts::link::add(ctx, &paths, force, store.root.as_deref()).map(|()| done),
|
||||||
cli::Link::Restore { paths, all, store } => {
|
cli::Link::Restore { paths, all, store } => {
|
||||||
scripts::link::restore(&paths, all, store.root.as_deref()).map(|()| done)
|
scripts::link::restore(ctx, &paths, all, store.root.as_deref()).map(|()| done)
|
||||||
}
|
}
|
||||||
// the one command with something to say through its exit code
|
// the one command with something to say through its exit code
|
||||||
cli::Link::Check {
|
cli::Link::Check {
|
||||||
@@ -78,7 +78,14 @@ fn run(ctx: &Ctx, command: cli::Commands) -> Result<ExitCode> {
|
|||||||
null,
|
null,
|
||||||
exit_code,
|
exit_code,
|
||||||
store,
|
store,
|
||||||
} => scripts::link::check(&paths, porcelain, null, exit_code, store.root.as_deref()),
|
} => scripts::link::check(
|
||||||
|
ctx,
|
||||||
|
&paths,
|
||||||
|
porcelain,
|
||||||
|
null,
|
||||||
|
exit_code,
|
||||||
|
store.root.as_deref(),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
cli::Commands::Completions { shell } => {
|
cli::Commands::Completions { shell } => {
|
||||||
scripts::completions::completions(shell)?;
|
scripts::completions::completions(shell)?;
|
||||||
|
|||||||
@@ -4,26 +4,29 @@ use std::ffi::OsString;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
|
||||||
|
use crate::cmd::{CheckIgnore, Cmd, ConfigGet, LsFiles, RevParse};
|
||||||
|
use crate::ctx::Ctx;
|
||||||
|
|
||||||
const BACKUP_SUFFIX: &str = ".ahab-bak";
|
const BACKUP_SUFFIX: &str = ".ahab-bak";
|
||||||
const LOCAL_NAMESPACE: &str = "_local";
|
const LOCAL_NAMESPACE: &str = "_local";
|
||||||
|
|
||||||
// move untracked paths out of the repo and symlink them back
|
// move untracked paths out of the repo and symlink them back
|
||||||
pub fn add(paths: &[PathBuf], force: bool, store: Option<&Path>) -> Result<()> {
|
pub fn add(ctx: &Ctx, paths: &[PathBuf], force: bool, store: Option<&Path>) -> Result<()> {
|
||||||
let repo = Repo::discover(store)?;
|
let repo = Repo::discover(ctx, store)?;
|
||||||
let report = Report::new(&repo);
|
let report = Report::new(&repo);
|
||||||
|
|
||||||
if let [path] = paths {
|
if let [path] = paths {
|
||||||
return link_one(&repo, path, force, &report);
|
return link_one(ctx, &repo, path, force, &report);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut failed = 0;
|
let mut failed = 0;
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if let Err(e) = link_one(&repo, path, force, &report) {
|
if let Err(e) = link_one(ctx, &repo, path, force, &report) {
|
||||||
eprintln!("error: {e:#}");
|
eprintln!("error: {e:#}");
|
||||||
failed += 1;
|
failed += 1;
|
||||||
}
|
}
|
||||||
@@ -36,8 +39,8 @@ pub fn add(paths: &[PathBuf], force: bool, store: Option<&Path>) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// move paths in the store back into the repo, the inverse of add
|
// move paths in the store back into the repo, the inverse of add
|
||||||
pub fn restore(paths: &[PathBuf], all: bool, store: Option<&Path>) -> Result<()> {
|
pub fn restore(ctx: &Ctx, paths: &[PathBuf], all: bool, store: Option<&Path>) -> Result<()> {
|
||||||
let repo = Repo::discover(store)?;
|
let repo = Repo::discover(ctx, store)?;
|
||||||
let report = Report::new(&repo);
|
let report = Report::new(&repo);
|
||||||
|
|
||||||
let paths = match (all, paths) {
|
let paths = match (all, paths) {
|
||||||
@@ -179,19 +182,20 @@ fn warn(msg: impl std::fmt::Display) {
|
|||||||
|
|
||||||
// list untracked paths not in the store, i.e. what a sandbox can still read
|
// list untracked paths not in the store, i.e. what a sandbox can still read
|
||||||
pub fn check(
|
pub fn check(
|
||||||
|
ctx: &Ctx,
|
||||||
paths: &[PathBuf],
|
paths: &[PathBuf],
|
||||||
porcelain: bool,
|
porcelain: bool,
|
||||||
null: bool,
|
null: bool,
|
||||||
exit_code: bool,
|
exit_code: bool,
|
||||||
store: Option<&Path>,
|
store: Option<&Path>,
|
||||||
) -> Result<ExitCode> {
|
) -> Result<ExitCode> {
|
||||||
let repo = Repo::discover(store)?;
|
let repo = Repo::discover(ctx, store)?;
|
||||||
let pathspecs = relative_pathspecs(&repo, paths)?;
|
let pathspecs = relative_pathspecs(&repo, paths)?;
|
||||||
|
|
||||||
let mut exposed = Vec::new();
|
let mut exposed = Vec::new();
|
||||||
// git lists untracked and ignored separately
|
// git lists untracked and ignored separately
|
||||||
for (mark, ignored) in [(UNTRACKED, false), (IGNORED, true)] {
|
for (mark, ignored) in [(UNTRACKED, false), (IGNORED, true)] {
|
||||||
for entry in list_others(&repo, ignored, &pathspecs)? {
|
for entry in list_others(ctx, &repo, ignored, &pathspecs)? {
|
||||||
let rel = PathBuf::from(entry.trim_end_matches('/'));
|
let rel = PathBuf::from(entry.trim_end_matches('/'));
|
||||||
|
|
||||||
// --directory collapses a wholly untracked dir into `dir/`
|
// --directory collapses a wholly untracked dir into `dir/`
|
||||||
@@ -392,30 +396,19 @@ fn relative_pathspecs(repo: &Repo, paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
|
|||||||
Ok(specs)
|
Ok(specs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_others(repo: &Repo, ignored: bool, pathspecs: &[PathBuf]) -> Result<Vec<String>> {
|
fn list_others(
|
||||||
let mut cmd = Command::new("git");
|
ctx: &Ctx,
|
||||||
cmd.arg("-C").arg(&repo.root).args([
|
repo: &Repo,
|
||||||
"ls-files",
|
ignored: bool,
|
||||||
"-z",
|
pathspecs: &[PathBuf],
|
||||||
"--others",
|
) -> Result<Vec<String>> {
|
||||||
"--exclude-standard",
|
let mut listing = LsFiles::untracked(&repo.root).limited_to(pathspecs);
|
||||||
"--directory",
|
|
||||||
"--no-empty-directory",
|
|
||||||
]);
|
|
||||||
if ignored {
|
if ignored {
|
||||||
cmd.arg("--ignored");
|
listing = listing.ignored();
|
||||||
}
|
|
||||||
cmd.arg("--").args(pathspecs);
|
|
||||||
|
|
||||||
let out = cmd.output().context("running git ls-files")?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"git ls-files failed: {}",
|
|
||||||
String::from_utf8_lossy(&out.stderr).trim()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(String::from_utf8(out.stdout)?
|
Ok(listing
|
||||||
|
.capture(ctx)?
|
||||||
.split('\0')
|
.split('\0')
|
||||||
.filter(|p| !p.is_empty())
|
.filter(|p| !p.is_empty())
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
@@ -430,15 +423,15 @@ struct Repo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Repo {
|
impl Repo {
|
||||||
fn discover(store: Option<&Path>) -> Result<Self> {
|
fn discover(ctx: &Ctx, store: Option<&Path>) -> Result<Self> {
|
||||||
let root = git_root()?;
|
let root = git_root(ctx)?;
|
||||||
let base = match store {
|
let base = match store {
|
||||||
Some(store) => store.to_path_buf(),
|
Some(store) => store.to_path_buf(),
|
||||||
None => store_root()?,
|
None => store_root()?,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
store: base.join(repo_components(&root)?),
|
store: base.join(repo_components(ctx, &root)?),
|
||||||
root,
|
root,
|
||||||
base,
|
base,
|
||||||
})
|
})
|
||||||
@@ -466,7 +459,7 @@ impl Repo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn link_one(repo: &Repo, path: &Path, force: bool, report: &Report) -> Result<()> {
|
fn link_one(ctx: &Ctx, repo: &Repo, path: &Path, force: bool, report: &Report) -> Result<()> {
|
||||||
let src = resolve(path)?;
|
let src = resolve(path)?;
|
||||||
let rel = repo.relative(&src)?;
|
let rel = repo.relative(&src)?;
|
||||||
let target = repo.store.join(&rel);
|
let target = repo.store.join(&rel);
|
||||||
@@ -479,13 +472,13 @@ fn link_one(repo: &Repo, path: &Path, force: bool, report: &Report) -> Result<()
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if tracked(repo, &rel)? {
|
if tracked(ctx, repo, &rel)? {
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"{} is tracked by git; only untracked or ignored paths can be externalized",
|
"{} is tracked by git; only untracked or ignored paths can be externalized",
|
||||||
rel.display()
|
rel.display()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !ignored(repo, &rel) {
|
if !ignored(ctx, repo, &rel) {
|
||||||
warn(format!("{} is not gitignored", rel.display()));
|
warn(format!("{} is not gitignored", rel.display()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,17 +594,12 @@ fn resolve(path: &Path) -> Result<PathBuf> {
|
|||||||
Ok(parent.join(name))
|
Ok(parent.join(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_root() -> Result<PathBuf> {
|
fn git_root(ctx: &Ctx) -> Result<PathBuf> {
|
||||||
let out = Command::new("git")
|
let root = RevParse
|
||||||
.args(["rev-parse", "--show-toplevel"])
|
.capture(ctx)
|
||||||
.output()
|
.map_err(|_| anyhow!("not inside a git repository"))?;
|
||||||
.context("running git")?;
|
|
||||||
|
|
||||||
if !out.status.success() {
|
let root = root.trim().to_string();
|
||||||
return Err(anyhow!("not inside a git repository"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let root = String::from_utf8(out.stdout)?.trim().to_string();
|
|
||||||
if root.is_empty() {
|
if root.is_empty() {
|
||||||
return Err(anyhow!("git reported an empty repository root"));
|
return Err(anyhow!("git reported an empty repository root"));
|
||||||
}
|
}
|
||||||
@@ -619,8 +607,8 @@ fn git_root() -> Result<PathBuf> {
|
|||||||
fs::canonicalize(&root).with_context(|| format!("resolving {root}"))
|
fs::canonicalize(&root).with_context(|| format!("resolving {root}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repo_components(root: &Path) -> Result<PathBuf> {
|
fn repo_components(ctx: &Ctx, root: &Path) -> Result<PathBuf> {
|
||||||
if let Some(url) = git_origin_url() {
|
if let Some(url) = git_origin_url(ctx) {
|
||||||
if let Some(components) = components_from_remote(&url) {
|
if let Some(components) = components_from_remote(&url) {
|
||||||
return Ok(components);
|
return Ok(components);
|
||||||
}
|
}
|
||||||
@@ -633,16 +621,14 @@ fn repo_components(root: &Path) -> Result<PathBuf> {
|
|||||||
Ok(Path::new(LOCAL_NAMESPACE).join(sanitize(&name.to_string_lossy())))
|
Ok(Path::new(LOCAL_NAMESPACE).join(sanitize(&name.to_string_lossy())))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_origin_url() -> Option<String> {
|
fn git_origin_url(ctx: &Ctx) -> Option<String> {
|
||||||
let out = Command::new("git")
|
let url = ConfigGet {
|
||||||
.args(["config", "--get", "remote.origin.url"])
|
key: "remote.origin.url",
|
||||||
.output()
|
|
||||||
.ok()?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
.capture(ctx)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
let url = String::from_utf8(out.stdout).ok()?.trim().to_string();
|
let url = url.trim().to_string();
|
||||||
(!url.is_empty()).then_some(url)
|
(!url.is_empty()).then_some(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,32 +692,18 @@ fn non_empty_var(name: &str) -> Option<OsString> {
|
|||||||
env::var_os(name).filter(|v| !v.is_empty())
|
env::var_os(name).filter(|v| !v.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tracked(repo: &Repo, rel: &Path) -> Result<bool> {
|
fn tracked(ctx: &Ctx, repo: &Repo, rel: &Path) -> Result<bool> {
|
||||||
let out = Command::new("git")
|
let listed = LsFiles::tracked(&repo.root)
|
||||||
.arg("-C")
|
.limited_to(&[rel])
|
||||||
.arg(&repo.root)
|
.capture(ctx)?;
|
||||||
.args(["ls-files", "--cached", "--"])
|
|
||||||
.arg(rel)
|
|
||||||
.output()
|
|
||||||
.context("running git ls-files")?;
|
|
||||||
|
|
||||||
if !out.status.success() {
|
Ok(!listed.is_empty())
|
||||||
return Err(anyhow!(
|
|
||||||
"git ls-files failed: {}",
|
|
||||||
String::from_utf8_lossy(&out.stderr).trim()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(!out.stdout.is_empty())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ignored(repo: &Repo, rel: &Path) -> bool {
|
fn ignored(ctx: &Ctx, repo: &Repo, rel: &Path) -> bool {
|
||||||
Command::new("git")
|
CheckIgnore::new(&repo.root, rel)
|
||||||
.arg("-C")
|
.quietly_succeeds(ctx)
|
||||||
.arg(&repo.root)
|
.unwrap_or(false)
|
||||||
.args(["check-ignore", "-q", "--"])
|
|
||||||
.arg(rel)
|
|
||||||
.status()
|
|
||||||
.is_ok_and(|s| s.success())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn symlink_metadata_opt(path: &Path) -> Result<Option<fs::Metadata>> {
|
fn symlink_metadata_opt(path: &Path) -> Result<Option<fs::Metadata>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user