use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::str;
use tempfile::NamedTempFile;
use tracing::warn;
use crate::errors::EditError;
use crate::out::Out;
#[cfg(windows)]
fn git_sh_path() -> Option<PathBuf> {
let output = Command::new("where").arg("git").output().ok()?;
if !output.status.success() {
return None;
}
Path::new(str::from_utf8(&output.stdout).ok()?.trim())
.canonicalize()
.ok()?
.parent()?
.parent()?
.join(r"bin\sh.exe")
.canonicalize()
.ok()
}
#[cfg(not(windows))]
fn git_sh_path() -> Option<PathBuf> {
Some("/bin/sh".into())
}
fn git_config(config: &str) -> Option<String> {
let output = Command::new("git")
.arg("config")
.arg(config)
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(str::from_utf8(&output.stdout).ok()?.trim().to_owned())
}
fn git_config_bool(config: &str) -> Option<bool> {
let value = git_config(config)?;
match &value[..] {
"true" | "yes" | "on" => Some(true),
"" | "false" | "no" | "off" => Some(false),
_ => Some(value.parse::<i64>().ok()? != 0),
}
}
fn git_editor() -> Option<String> {
if std::env::var("CARGO_VET_USE_FALLBACK_EDITOR").unwrap_or_default() == "1" {
return None;
}
let output = Command::new("git")
.arg("var")
.arg("GIT_EDITOR")
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(str::from_utf8(&output.stdout).ok()?.trim().to_owned())
}
#[cfg(windows)]
const FALLBACK_EDITOR: &str = "notepad.exe";
#[cfg(not(windows))]
const FALLBACK_EDITOR: &str = "nano";
pub fn editor_command() -> Command {
match (git_sh_path(), git_editor()) {
(Some(git_sh), Some(git_editor)) => {
let mut cmd = Command::new(git_sh);
cmd.arg("-c")
.arg(format!("{git_editor} \"$@\""))
.arg(git_editor);
return cmd;
}
(_, None) => {
warn!("Unable to determine user's GIT_EDITOR");
}
(None, Some(_)) => {
warn!("Unable to locate user's git install to invoke GIT_EDITOR");
}
}
warn!("Falling back to running '{}' directly", FALLBACK_EDITOR);
Command::new(FALLBACK_EDITOR)
}
pub fn run_editor(path: &Path) -> io::Result<ExitStatus> {
editor_command().arg(path).status()
}
#[cfg(windows)]
const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &str = "\n";
pub struct Editor<'a> {
tempfile: NamedTempFile,
comment_char: char,
#[allow(clippy::type_complexity)]
run_editor: Box<dyn FnOnce(&Path) -> io::Result<bool> + 'a>,
}
impl Editor<'_> {
pub fn new(name: &str) -> io::Result<Self> {
let tempfile = tempfile::Builder::new()
.prefix(&format!("{name}."))
.tempfile()?;
Ok(Editor {
tempfile,
comment_char: '#',
run_editor: Box::new(|p| run_editor(p).map(|s| s.success())),
})
}
pub fn set_comment_char(&mut self, comment_char: char) {
self.comment_char = comment_char;
}
pub fn select_comment_char(&mut self, text: &str) {
let mut comment_chars = ['#', ';', '@', '!', '$', '%', '^', '&', '|', ':', '"', ';'];
for line in text.lines() {
for cc in &mut comment_chars {
if line.starts_with(*cc) {
*cc = '\0';
break;
}
}
}
self.set_comment_char(
comment_chars
.into_iter()
.find(|cc| *cc != '\0')
.expect("couldn't find a viable comment character"),
);
}
pub fn add_comments(&mut self, text: &str) -> io::Result<()> {
let text = text.trim();
if text.is_empty() {
write!(self.tempfile, "{}{}", self.comment_char, LINE_ENDING)?;
}
for line in text.lines() {
if line.is_empty() {
write!(self.tempfile, "{}{}", self.comment_char, LINE_ENDING)?;
} else {
write!(
self.tempfile,
"{} {}{}",
self.comment_char, line, LINE_ENDING
)?;
}
}
Ok(())
}
pub fn add_text(&mut self, text: &str) -> io::Result<()> {
let text = text.trim();
if text.is_empty() {
write!(self.tempfile, "{LINE_ENDING}")?;
}
for line in text.lines() {
assert!(
!line.starts_with(self.comment_char),
"non-comment lines cannot start with a '{}' comment character",
self.comment_char
);
write!(self.tempfile, "{line}{LINE_ENDING}")?;
}
Ok(())
}
pub fn edit(self) -> Result<String, EditError> {
let path = self.tempfile.into_temp_path();
(self.run_editor)(&path).map_err(EditError::CouldntLaunch)?;
let mut lines: Vec<String> = Vec::new();
for line in BufReader::new(File::open(&path).map_err(EditError::CouldntOpen)?).lines() {
let line = line.map_err(EditError::CouldntRead)?;
if line.starts_with(self.comment_char) {
continue;
}
let line = line.trim_end();
if line.is_empty() && lines.last().is_none_or(|l| l.is_empty()) {
continue;
}
lines.push(line.to_owned());
}
match lines.last() {
None => return Ok(String::new()),
Some(line) if !line.is_empty() => lines.push(String::new()),
_ => {}
}
Ok(lines.join("\n"))
}
}
#[cfg(test)]
impl<'a> Editor<'a> {
pub fn set_run_editor(&mut self, run_editor: impl FnOnce(&Path) -> io::Result<bool> + 'a) {
self.run_editor = Box::new(run_editor);
}
}
fn git_pager() -> Option<String> {
let output = Command::new("git")
.arg("var")
.arg("GIT_PAGER")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let pager = str::from_utf8(&output.stdout).ok()?.trim().to_owned();
if pager == "cat" {
return None;
}
Some(pager)
}
fn pager_command(out: &dyn Out) -> Option<Command> {
if !out.is_term() {
return None;
}
let git_sh = git_sh_path()?;
let git_pager = git_pager()?;
let mut cmd = Command::new(git_sh);
cmd.arg("-c")
.arg(format!("{git_pager} \"$@\""))
.arg(git_pager);
cmd.env("LESS", "FRX").env("LV", "-c");
Some(cmd)
}
pub struct Pager<'a> {
out: &'a dyn Out,
child: Option<Child>,
use_color: bool,
}
impl<'a> Pager<'a> {
pub fn new(out: &'a dyn Out) -> Result<Self, io::Error> {
let child = if let Some(mut cmd) = pager_command(out) {
Some(cmd.stdin(Stdio::piped()).spawn()?)
} else {
None
};
let use_color = child.is_some()
&& out.is_term()
&& console::colors_enabled()
&& git_config_bool("pager.color")
.or_else(|| git_config_bool("color.pager"))
.unwrap_or(true);
Ok(Pager {
out,
child,
use_color,
})
}
pub fn use_color(&self) -> bool {
self.use_color
}
pub fn wait(self) -> Result<(), io::Error> {
if let Some(mut child) = self.child {
child.wait()?;
}
Ok(())
}
}
impl io::Write for Pager<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match &mut self.child {
Some(child) => child.stdin.as_mut().unwrap().write(buf),
None => self.out.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match &mut self.child {
Some(child) => child.stdin.as_mut().unwrap().flush(),
None => Ok(()),
}
}
}