use std::borrow::Cow;
use std::path::Path;
use unicode_segmentation::UnicodeSegmentation;
use super::draw;
use crate::config::Config;
use crate::delta::{DiffType, Source, State, StateMachine};
use crate::paint::Painter;
use crate::{features, utils};
const DIFF_PREFIXES: [&str; 6] = ["a/", "b/", "c/", "i/", "o/", "w/"];
#[derive(Debug, PartialEq)]
pub enum FileEvent {
Change,
Copy,
Rename,
NoEvent,
}
impl<'a> StateMachine<'a> {
pub fn handle_diff_header_mode_line(&mut self) -> std::io::Result<bool> {
let mut handled_line = false;
if let Some(line_suf) = self.line.strip_prefix("old mode ") {
self.state = State::DiffHeader(DiffType::Unified);
if self.should_handle() && !self.config.color_only {
self.mode_info = line_suf.to_string();
handled_line = true;
}
} else if let Some(line_suf) = self.line.strip_prefix("new mode ") {
self.state = State::DiffHeader(DiffType::Unified);
if self.should_handle() && !self.config.color_only && !self.mode_info.is_empty() {
self.mode_info = match (self.mode_info.as_str(), line_suf) {
("100644", "100755") => "mode +x".to_string(),
("100755", "100644") => "mode -x".to_string(),
_ => format!(
"mode {} {} {}",
self.mode_info, self.config.right_arrow, line_suf
),
};
handled_line = true;
}
}
Ok(handled_line)
}
#[inline]
fn test_diff_header_minus_line(&self) -> bool {
(matches!(self.state, State::DiffHeader(_)) || self.source == Source::DiffUnified)
&& (self.line.starts_with("--- ")
|| self.line.starts_with("rename from ")
|| self.line.starts_with("copy from "))
}
pub fn handle_diff_header_minus_line(&mut self) -> std::io::Result<bool> {
if !self.test_diff_header_minus_line() {
return Ok(false);
}
let mut handled_line = false;
let (path_or_mode, file_event) =
parse_diff_header_line(&self.line, self.source == Source::GitDiff);
self.minus_file = utils::path::relativize_path_maybe(&path_or_mode, self.config)
.map(|p| p.to_string_lossy().to_owned().to_string())
.unwrap_or(path_or_mode);
self.minus_file_event = file_event;
if self.source == Source::DiffUnified {
self.state = State::DiffHeader(DiffType::Unified);
self.painter
.set_syntax(get_file_extension_from_marker_line(&self.line));
} else {
self.painter
.set_syntax(get_file_extension_from_diff_header_line_file_path(
&self.minus_file,
));
}
self.painter.paint_buffered_minus_and_plus_lines();
if self.config.color_only {
write_generic_diff_header_header_line(
&self.line,
&self.raw_line,
&mut self.painter,
&mut self.mode_info,
self.config,
)?;
handled_line = true;
}
Ok(handled_line)
}
#[inline]
fn test_diff_header_plus_line(&self) -> bool {
(matches!(self.state, State::DiffHeader(_)) || self.source == Source::DiffUnified)
&& (self.line.starts_with("+++ ")
|| self.line.starts_with("rename to ")
|| self.line.starts_with("copy to "))
}
pub fn handle_diff_header_plus_line(&mut self) -> std::io::Result<bool> {
if !self.test_diff_header_plus_line() {
return Ok(false);
}
let mut handled_line = false;
let (path_or_mode, file_event) =
parse_diff_header_line(&self.line, self.source == Source::GitDiff);
self.plus_file = utils::path::relativize_path_maybe(&path_or_mode, self.config)
.map(|p| p.to_string_lossy().to_owned().to_string())
.unwrap_or(path_or_mode);
self.plus_file_event = file_event;
self.painter
.set_syntax(get_file_extension_from_diff_header_line_file_path(
&self.plus_file,
));
self.current_file_pair = Some((self.minus_file.clone(), self.plus_file.clone()));
self.painter.paint_buffered_minus_and_plus_lines();
if self.config.color_only {
write_generic_diff_header_header_line(
&self.line,
&self.raw_line,
&mut self.painter,
&mut self.mode_info,
self.config,
)?;
handled_line = true
} else if self.should_handle()
&& self.handled_diff_header_header_line_file_pair != self.current_file_pair
{
self.painter.emit()?;
self._handle_diff_header_header_line(self.source == Source::DiffUnified)?;
self.handled_diff_header_header_line_file_pair = self.current_file_pair.clone()
}
Ok(handled_line)
}
fn _handle_diff_header_header_line(&mut self, comparing: bool) -> std::io::Result<()> {
let line = get_file_change_description_from_file_paths(
&self.minus_file,
&self.plus_file,
comparing,
&self.minus_file_event,
&self.plus_file_event,
self.config,
);
write_generic_diff_header_header_line(
&line,
&line,
&mut self.painter,
&mut self.mode_info,
self.config,
)
}
pub fn handle_pending_mode_line_with_diff_name(&mut self) -> std::io::Result<()> {
if !self.mode_info.is_empty() {
let format_label = |label: &str| {
if !label.is_empty() {
format!("{} ", label)
} else {
"".to_string()
}
};
let format_file = |file| match (
self.config.hyperlinks,
utils::path::absolute_path(file, self.config),
) {
(true, Some(absolute_path)) => features::hyperlinks::format_osc8_file_hyperlink(
absolute_path,
None,
file,
self.config,
),
_ => Cow::from(file),
};
let label = format_label(&self.config.file_modified_label);
let name = get_repeated_file_path_from_diff_line(&self.diff_line)
.unwrap_or_else(|| "".to_string());
let line = format!("{}{}", label, format_file(&name));
write_generic_diff_header_header_line(
&line,
&line,
&mut self.painter,
&mut self.mode_info,
self.config,
)
} else {
Ok(())
}
}
}
pub fn write_generic_diff_header_header_line(
line: &str,
raw_line: &str,
painter: &mut Painter,
mode_info: &mut String,
config: &Config,
) -> std::io::Result<()> {
if config.file_style.is_omitted && !config.color_only {
return Ok(());
}
let (mut draw_fn, pad, decoration_ansi_term_style) =
draw::get_draw_function(config.file_style.decoration_style);
if !config.color_only {
writeln!(painter.writer)?;
}
draw_fn(
painter.writer,
&format!("{}{}", line, if pad { " " } else { "" }),
&format!("{}{}", raw_line, if pad { " " } else { "" }),
mode_info,
&config.decorations_width,
config.file_style,
decoration_ansi_term_style,
)?;
if !mode_info.is_empty() {
mode_info.truncate(0);
}
Ok(())
}
#[allow(clippy::tabs_in_doc_comments)]
fn get_file_extension_from_marker_line(line: &str) -> Option<&str> {
line.split('\t')
.next()
.and_then(|column| column.split(' ').nth(1))
.and_then(|file| file.split('.').last())
}
fn get_file_extension_from_diff_header_line_file_path(path: &str) -> Option<&str> {
if path.is_empty() || path == "/dev/null" {
None
} else {
get_extension(path).map(|ex| ex.trim())
}
}
pub fn get_extension(s: &str) -> Option<&str> {
let path = Path::new(s);
path.extension()
.and_then(|e| e.to_str())
.or_else(|| path.file_name().and_then(|s| s.to_str()))
}
fn parse_diff_header_line(line: &str, git_diff_name: bool) -> (String, FileEvent) {
match line {
line if line.starts_with("--- ") || line.starts_with("+++ ") => {
let offset = 4;
let file = _parse_file_path(&line[offset..], git_diff_name);
(file, FileEvent::Change)
}
line if line.starts_with("rename from ") => {
(line[12..].to_string(), FileEvent::Rename) }
line if line.starts_with("rename to ") => {
(line[10..].to_string(), FileEvent::Rename) }
line if line.starts_with("copy from ") => {
(line[10..].to_string(), FileEvent::Copy) }
line if line.starts_with("copy to ") => {
(line[8..].to_string(), FileEvent::Copy) }
_ => ("".to_string(), FileEvent::NoEvent),
}
}
fn get_repeated_file_path_from_diff_line(line: &str) -> Option<String> {
if let Some(line) = line.strip_prefix("diff --git ") {
let line: Vec<&str> = line.graphemes(true).collect();
let midpoint = line.len() / 2;
if line[midpoint] == " " {
let first_path = _parse_file_path(&line[..midpoint].join(""), true);
let second_path = _parse_file_path(&line[midpoint + 1..].join(""), true);
if first_path == second_path {
return Some(first_path);
}
}
}
None
}
fn _parse_file_path(s: &str, git_diff_name: bool) -> String {
match s.strip_suffix('\t').unwrap_or(s) {
path if path == "/dev/null" => "/dev/null",
path if git_diff_name && DIFF_PREFIXES.iter().any(|s| path.starts_with(s)) => &path[2..],
path if git_diff_name => path,
path => path.split('\t').next().unwrap_or(""),
}
.to_string()
}
pub fn get_file_change_description_from_file_paths(
minus_file: &str,
plus_file: &str,
comparing: bool,
minus_file_event: &FileEvent,
plus_file_event: &FileEvent,
config: &Config,
) -> String {
let format_label = |label: &str| {
if !label.is_empty() {
format!("{} ", label)
} else {
"".to_string()
}
};
if comparing {
format!(
"{}{} {} {}",
format_label(&config.file_modified_label),
minus_file,
config.right_arrow,
plus_file
)
} else {
let format_file = |file| match (config.hyperlinks, utils::path::absolute_path(file, config))
{
(true, Some(absolute_path)) => {
features::hyperlinks::format_osc8_file_hyperlink(absolute_path, None, file, config)
}
_ => Cow::from(file),
};
match (minus_file, plus_file, minus_file_event, plus_file_event) {
(minus_file, plus_file, _, _) if minus_file == plus_file => format!(
"{}{}",
format_label(&config.file_modified_label),
format_file(minus_file)
),
(minus_file, "/dev/null", _, _) => format!(
"{}{}",
format_label(&config.file_removed_label),
format_file(minus_file)
),
("/dev/null", plus_file, _, _) => format!(
"{}{}",
format_label(&config.file_added_label),
format_file(plus_file)
),
(minus_file, plus_file, file_event, _) => format!(
"{}{} {} {}",
format_label(match file_event {
FileEvent::Rename => &config.file_renamed_label,
FileEvent::Copy => &config.file_copied_label,
_ => &config.file_modified_label,
}),
format_file(minus_file),
config.right_arrow,
format_file(plus_file)
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_file_extension_from_marker_line() {
assert_eq!(
get_file_extension_from_marker_line(
"--- src/one.rs 2019-11-20 06:47:56.000000000 +0100"
),
Some("rs")
);
}
#[test]
fn test_get_file_extension_from_diff_header_line() {
assert_eq!(
get_file_extension_from_diff_header_line_file_path("a/src/parse.rs"),
Some("rs")
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("b/src/pa rse.rs"),
Some("rs")
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("src/pa rse.rs"),
Some("rs")
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("wat hello.rs"),
Some("rs")
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("/dev/null"),
None
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("Dockerfile"),
Some("Dockerfile")
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("Makefile"),
Some("Makefile")
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("a/src/Makefile"),
Some("Makefile")
);
assert_eq!(
get_file_extension_from_diff_header_line_file_path("src/Makefile"),
Some("Makefile")
);
}
#[test]
fn test_get_file_path_from_git_diff_header_line() {
assert_eq!(
parse_diff_header_line("--- /dev/null", true),
("/dev/null".to_string(), FileEvent::Change)
);
for prefix in &DIFF_PREFIXES {
assert_eq!(
parse_diff_header_line(&format!("--- {}src/delta.rs", prefix), true),
("src/delta.rs".to_string(), FileEvent::Change)
);
}
assert_eq!(
parse_diff_header_line("--- src/delta.rs", true),
("src/delta.rs".to_string(), FileEvent::Change)
);
assert_eq!(
parse_diff_header_line("+++ src/delta.rs", true),
("src/delta.rs".to_string(), FileEvent::Change)
);
}
#[test]
fn test_get_file_path_from_git_diff_header_line_containing_spaces() {
assert_eq!(
parse_diff_header_line("+++ a/my src/delta.rs", true),
("my src/delta.rs".to_string(), FileEvent::Change)
);
assert_eq!(
parse_diff_header_line("+++ my src/delta.rs", true),
("my src/delta.rs".to_string(), FileEvent::Change)
);
assert_eq!(
parse_diff_header_line("+++ a/src/my delta.rs", true),
("src/my delta.rs".to_string(), FileEvent::Change)
);
assert_eq!(
parse_diff_header_line("+++ a/my src/my delta.rs", true),
("my src/my delta.rs".to_string(), FileEvent::Change)
);
assert_eq!(
parse_diff_header_line("+++ b/my src/my enough/my delta.rs", true),
(
"my src/my enough/my delta.rs".to_string(),
FileEvent::Change
)
);
}
#[test]
fn test_get_file_path_from_git_diff_header_line_rename() {
assert_eq!(
parse_diff_header_line("rename from nospace/file2.el", true),
("nospace/file2.el".to_string(), FileEvent::Rename)
);
}
#[test]
fn test_get_file_path_from_git_diff_header_line_rename_containing_spaces() {
assert_eq!(
parse_diff_header_line("rename from with space/file1.el", true),
("with space/file1.el".to_string(), FileEvent::Rename)
);
}
#[test]
fn test_parse_diff_header_line() {
assert_eq!(
parse_diff_header_line("--- src/delta.rs", false),
("src/delta.rs".to_string(), FileEvent::Change)
);
assert_eq!(
parse_diff_header_line("+++ src/delta.rs", false),
("src/delta.rs".to_string(), FileEvent::Change)
);
}
#[test]
fn test_get_repeated_file_path_from_diff_line() {
assert_eq!(
get_repeated_file_path_from_diff_line("diff --git a/src/main.rs b/src/main.rs"),
Some("src/main.rs".to_string())
);
assert_eq!(
get_repeated_file_path_from_diff_line("diff --git a/a b/a"),
Some("a".to_string())
);
assert_eq!(
get_repeated_file_path_from_diff_line("diff --git a/a b b/a b"),
Some("a b".to_string())
);
assert_eq!(
get_repeated_file_path_from_diff_line("diff --git a/a b/aa"),
None
);
assert_eq!(
get_repeated_file_path_from_diff_line(
"diff --git a/.config/Code - Insiders/User/settings.json b/.config/Code - Insiders/User/settings.json"),
Some(".config/Code - Insiders/User/settings.json".to_string())
);
}
}