fix(direnv): accept null loadedRC state (#7317)

This commit is contained in:
acture
2026-04-04 00:06:30 +08:00
committed by GitHub
parent 7a274752d0
commit 56b8901f6f
+160 -34
View File
@@ -30,14 +30,19 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// the `--json` flag is silently ignored for direnv versions <2.33.0 // the `--json` flag is silently ignored for direnv versions <2.33.0
let direnv_status = &context.exec_cmd("direnv", &["status", "--json"])?.stdout; let direnv_status = &context.exec_cmd("direnv", &["status", "--json"])?.stdout;
let state = match DirenvState::from_str(direnv_status) { let state = serde_json::from_str::<RawDirenvState>(direnv_status).map_or_else(
Ok(s) => s, |_| {
Err(e) => { DirenvState::from_lines(direnv_status)
log::warn!("{e}"); .inspect_err(|e| log::warn!("{e}"))
.ok()
return None; },
} |raw| {
}; raw.into_direnv_state()
.inspect_err(|e| log::warn!("{e}"))
.ok()
.flatten()
},
)?;
let parsed = StringFormatter::new(config.format).and_then(|formatter| { let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter formatter
@@ -87,14 +92,9 @@ impl FromStr for DirenvState {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match serde_json::from_str::<RawDirenvState>(s) { match serde_json::from_str::<RawDirenvState>(s) {
Ok(raw) => Ok(Self { Ok(raw) => raw
rc_path: raw.state.found_rc.path, .into_direnv_state()?
allowed: raw.state.found_rc.allowed.try_into()?, .ok_or_else(|| Cow::from("unknown direnv state")),
loaded: matches!(
raw.state.loaded_rc.allowed.try_into()?,
AllowStatus::Allowed
),
}),
Err(_) => Self::from_lines(s), Err(_) => Self::from_lines(s),
} }
} }
@@ -128,7 +128,7 @@ impl DirenvState {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
enum AllowStatus { enum AllowStatus {
Allowed, Allowed,
NotAllowed, NotAllowed,
@@ -166,12 +166,31 @@ struct RawDirenvState {
pub state: State, pub state: State,
} }
impl RawDirenvState {
fn into_direnv_state(self) -> Result<Option<DirenvState>, Cow<'static, str>> {
match (self.state.found_rc, self.state.loaded_rc) {
(None, None) => Ok(None),
(Some(found_rc), None) => Ok(Some(DirenvState {
rc_path: found_rc.path,
allowed: found_rc.allowed.try_into()?,
loaded: false,
})),
(Some(found_rc), Some(loaded_rc)) => Ok(Some(DirenvState {
rc_path: found_rc.path,
allowed: found_rc.allowed.try_into()?,
loaded: matches!(loaded_rc.allowed.try_into()?, AllowStatus::Allowed),
})),
(None, Some(_)) => Err(Cow::from("unknown direnv state")),
}
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct State { struct State {
#[serde(rename = "foundRC")] #[serde(rename = "foundRC")]
pub found_rc: RCStatus, pub found_rc: Option<RCStatus>,
#[serde(rename = "loadedRC")] #[serde(rename = "loadedRC")]
pub loaded_rc: RCStatus, pub loaded_rc: Option<RCStatus>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -182,13 +201,52 @@ struct RCStatus {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use serde_json::json; use super::{AllowStatus, RawDirenvState};
use serde_json::{Value, json};
use crate::test::ModuleRenderer; use crate::test::ModuleRenderer;
use crate::utils::CommandOutput; use crate::utils::CommandOutput;
use nu_ansi_term::Color; use nu_ansi_term::Color;
use std::io; use std::io;
use std::path::Path; use std::path::Path;
#[test]
fn parses_found_but_unloaded_rc_json_state() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
let state = serde_json::from_str::<RawDirenvState>(&status_cmd_output_with_rc_json(
dir.path(),
Some(0),
None,
))
.expect("direnv JSON state should deserialize")
.into_direnv_state()
.expect("direnv JSON state should parse")
.expect("direnv state should be present");
assert_eq!(state.rc_path, rc_path);
assert_eq!(state.allowed, AllowStatus::Allowed);
assert!(!state.loaded);
dir.close()
}
#[test]
fn rejects_loaded_rc_without_found_rc_json_state() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let result = serde_json::from_str::<RawDirenvState>(&status_cmd_output_with_rc_json(
dir.path(),
None,
Some(0),
))
.expect("direnv JSON state should deserialize")
.into_direnv_state();
assert!(result.is_err());
dir.close()
}
#[test] #[test]
fn folder_without_rc_files_pre_2_33() { fn folder_without_rc_files_pre_2_33() {
let renderer = ModuleRenderer::new("direnv") let renderer = ModuleRenderer::new("direnv")
@@ -268,7 +326,7 @@ mod tests {
.cmd( .cmd(
"direnv status --json", "direnv status --json",
Some(CommandOutput { Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 1, 0), stdout: status_cmd_output_with_rc_json(dir.path(), Some(0), None),
stderr: String::default(), stderr: String::default(),
}), }),
) )
@@ -282,6 +340,66 @@ mod tests {
dir.close() dir.close()
} }
#[test] #[test]
fn folder_with_unloaded_and_not_allowed_rc_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let actual = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), Some(1), None),
stderr: String::default(),
}),
)
.collect();
let expected = Some(format!(
"{} ",
Color::LightYellow
.bold()
.paint("direnv not loaded/not allowed")
));
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn folder_with_unloaded_and_denied_rc_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let actual = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), Some(2), None),
stderr: String::default(),
}),
)
.collect();
let expected = Some(format!(
"{} ",
Color::LightYellow.bold().paint("direnv not loaded/denied")
));
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn folder_with_loaded_rc_file_pre_2_33() -> io::Result<()> { fn folder_with_loaded_rc_file_pre_2_33() -> io::Result<()> {
let dir = tempfile::tempdir()?; let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc"); let rc_path = dir.path().join(".envrc");
@@ -326,7 +444,7 @@ mod tests {
.cmd( .cmd(
"direnv status --json", "direnv status --json",
Some(CommandOutput { Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 0, 0), stdout: status_cmd_output_with_rc_json(dir.path(), Some(0), Some(0)),
stderr: String::default(), stderr: String::default(),
}), }),
) )
@@ -360,7 +478,7 @@ mod tests {
.cmd( .cmd(
"direnv status --json", "direnv status --json",
Some(CommandOutput { Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 0, 0), stdout: status_cmd_output_with_rc_json(dir.path(), Some(0), Some(0)),
stderr: String::default(), stderr: String::default(),
}), }),
) )
@@ -418,7 +536,7 @@ mod tests {
.cmd( .cmd(
"direnv status --json", "direnv status --json",
Some(CommandOutput { Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 0, 1), stdout: status_cmd_output_with_rc_json(dir.path(), Some(1), Some(0)),
stderr: String::default(), stderr: String::default(),
}), }),
) )
@@ -447,7 +565,7 @@ mod tests {
.cmd( .cmd(
"direnv status --json", "direnv status --json",
Some(CommandOutput { Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 0, 2), stdout: status_cmd_output_with_rc_json(dir.path(), Some(2), Some(0)),
stderr: String::default(), stderr: String::default(),
}), }),
) )
@@ -536,7 +654,11 @@ Found RC allowPath /home/test/.local/share/direnv/allow/abcd
"# "#
) )
} }
fn status_cmd_output_with_rc_json(dir: impl AsRef<Path>, loaded: u8, allowed: u8) -> String { fn status_cmd_output_with_rc_json(
dir: impl AsRef<Path>,
found_allowed: Option<u8>,
loaded_allowed: Option<u8>,
) -> String {
let rc_path = dir.as_ref().join(".envrc"); let rc_path = dir.as_ref().join(".envrc");
let rc_path = rc_path.to_string_lossy(); let rc_path = rc_path.to_string_lossy();
@@ -546,18 +668,22 @@ Found RC allowPath /home/test/.local/share/direnv/allow/abcd
"SelfPath": self_path(), "SelfPath": self_path(),
}, },
"state": { "state": {
"foundRC": { "foundRC": rc_status_json(&rc_path, found_allowed),
"allowed": allowed, "loadedRC": rc_status_json(&rc_path, loaded_allowed),
"path": rc_path,
},
"loadedRC": {
"allowed": loaded,
"path": rc_path,
}
} }
}) })
.to_string() .to_string()
} }
fn rc_status_json(rc_path: &str, allowed: Option<u8>) -> Value {
match allowed {
Some(allowed) => json!({
"allowed": allowed,
"path": rc_path,
}),
None => Value::Null,
}
}
#[cfg(windows)] #[cfg(windows)]
fn config_dir() -> &'static str { fn config_dir() -> &'static str {
r"C:\\Users\\test\\AppData\\Local\\direnv" r"C:\\Users\\test\\AppData\\Local\\direnv"