diff options
author | Dan Davison <dandavison7@gmail.com> | 2021-11-15 18:45:58 -0500 |
---|---|---|
committer | Dan Davison <dandavison7@gmail.com> | 2021-11-15 21:03:10 -0500 |
commit | 45c802528adcb5646ec5fe0d92b8e59119c80fb7 (patch) | |
tree | dde0969b8c3f27b4ba431c81550e9a947d4ac064 /src/utils | |
parent | 24a07ff9474dc8dc4942026b7d6c3804132cb047 (diff) |
Refactor: utils module
Diffstat (limited to 'src/utils')
-rw-r--r-- | src/utils/bat/LICENSE | 26 | ||||
-rw-r--r-- | src/utils/bat/assets.rs | 144 | ||||
-rw-r--r-- | src/utils/bat/dirs.rs | 41 | ||||
-rw-r--r-- | src/utils/bat/less.rs | 66 | ||||
-rw-r--r-- | src/utils/bat/mod.rs | 5 | ||||
-rw-r--r-- | src/utils/bat/output.rs | 204 | ||||
-rw-r--r-- | src/utils/bat/terminal.rs | 83 | ||||
-rw-r--r-- | src/utils/mod.rs | 4 | ||||
-rw-r--r-- | src/utils/process.rs | 304 | ||||
-rw-r--r-- | src/utils/syntect.rs | 86 |
10 files changed, 963 insertions, 0 deletions
diff --git a/src/utils/bat/LICENSE b/src/utils/bat/LICENSE new file mode 100644 index 00000000..8af3b8b8 --- /dev/null +++ b/src/utils/bat/LICENSE @@ -0,0 +1,26 @@ +Files under this directory (i.e. src/bat_utils/) originated as copies of +files from the `bat` project (https://github.com/sharkdp/bat), with +subsequent modifications, as recorded in git history. The `bat` +license is reproduced below. + +----------------------------------------------------------------------------- + +Copyright (c) 2018 bat-developers (https://github.com/sharkdp/bat). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/utils/bat/assets.rs b/src/utils/bat/assets.rs new file mode 100644 index 00000000..cfe0f5bd --- /dev/null +++ b/src/utils/bat/assets.rs @@ -0,0 +1,144 @@ +// Based on code from https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468 +// See src/utils/bat/LICENSE + +use std::fs::File; +use std::io::{self, BufReader, Write}; +use std::path::PathBuf; + +use ansi_term::Colour::Green; +use ansi_term::Style; +use syntect::dumps::{from_binary, from_reader}; +use syntect::highlighting::ThemeSet; +use syntect::parsing::SyntaxSet; + +use crate::errors::*; +use crate::utils::bat::dirs::PROJECT_DIRS; + +pub struct HighlightingAssets { + pub syntax_set: SyntaxSet, + pub theme_set: ThemeSet, +} + +impl HighlightingAssets { + pub fn new() -> Self { + Self::from_cache().unwrap_or_else(|_| Self::from_binary()) + } + + fn get_integrated_syntaxset() -> SyntaxSet { + from_binary(include_bytes!("../../../etc/assets/syntaxes.bin")) + } + + fn get_integrated_themeset() -> ThemeSet { + from_binary(include_bytes!("../../../etc/assets/themes.bin")) + } + + fn from_cache() -> Result<Self> { + let theme_set_path = theme_set_path(); + let syntax_set_file = File::open(&syntax_set_path()).chain_err(|| { + format!( + "Could not load cached syntax set '{}'", + syntax_set_path().to_string_lossy() + ) + })?; + let syntax_set: SyntaxSet = from_reader(BufReader::new(syntax_set_file)) + .chain_err(|| "Could not parse cached syntax set")?; + + let theme_set_file = File::open(&theme_set_path).chain_err(|| { + format!( + "Could not load cached theme set '{}'", + theme_set_path.to_string_lossy() + ) + })?; + let theme_set: ThemeSet = from_reader(BufReader::new(theme_set_file)) + .chain_err(|| "Could not parse cached theme set")?; + + Ok(HighlightingAssets { + syntax_set, + theme_set, + }) + } + + fn from_binary() -> Self { + let syntax_set = Self::get_integrated_syntaxset(); + let theme_set = Self::get_integrated_themeset(); + + HighlightingAssets { + syntax_set, + theme_set, + } + } +} + +fn theme_set_path() -> PathBuf { + PROJECT_DIRS.cache_dir().join("themes.bin") +} + +fn syntax_set_path() -> PathBuf { + PROJECT_DIRS.cache_dir().join("syntaxes.bin") +} + +pub fn list_languages() -> std::io::Result<()> { + let assets = HighlightingAssets::new(); + let mut languages = assets + .syntax_set + .syntaxes() + .iter() + .filter(|syntax| !syntax.hidden && !syntax.file_extensions.is_empty()) + .collect::<Vec<_>>(); + languages.sort_by_key(|lang| lang.name.to_uppercase()); + + let loop_through = false; + let colored_output = true; + + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + if loop_through { + for lang in languages { + writeln!(stdout, "{}:{}", lang.name, lang.file_extensions.join(","))?; + } + } else { + let longest = languages + .iter() + .map(|syntax| syntax.name.len()) + .max() + .unwrap_or(32); // Fallback width if they have no language definitions. + + let comma_separator = ", "; + let separator = " "; + // Line-wrapping for the possible file extension overflow. + let desired_width = 100; + + let style = if colored_output { + Green.normal() + } else { + Style::default() + }; + + for lang in languages { + write!(stdout, "{:width$}{}", lang.name, separator, width = longest)?; + + // Number of characters on this line so far, wrap before `desired_width` + let mut num_chars = 0; + + let mut extension = lang.file_extensions.iter().peekable(); + while let Some(word) = extension.next() { + // If we can't fit this word in, then create a line break and align it in. + let new_chars = word.len() + comma_separator.len(); + if num_chars + new_chars >= desired_width { + num_chars = 0; + write!(stdout, "\n{:width$}{}", "", separator, width = longest)?; + } + + num_chars += new_chars; + write!(stdout, "{}", style.paint(&word[..]))?; + if extension.peek().is_some() { + write!(stdout, "{}", comma_separator)?; + } + } + writeln!(stdout)?; + } + } + + Ok(()) +} diff --git a/src/utils/bat/dirs.rs b/src/utils/bat/dirs.rs new file mode 100644 index 00000000..efaef81f --- /dev/null +++ b/src/utils/bat/dirs.rs @@ -0,0 +1,41 @@ +// Based on code from https://github.com/sharkdp/bat e981e974076a926a38f124b7d8746de2ca5f0a28 +// See src/utils/bat/LICENSE + +use lazy_static::lazy_static; +use std::path::{Path, PathBuf}; + +#[cfg(target_os = "macos")] +use std::env; + +/// Wrapper for 'dirs' that treats MacOS more like Linux, by following the XDG specification. +/// This means that the `XDG_CACHE_HOME` and `XDG_CONFIG_HOME` environment variables are +/// checked first. The fallback directories are `~/.cache/bat` and `~/.config/bat`, respectively. +pub struct BatProjectDirs { + cache_dir: PathBuf, +} + +impl BatProjectDirs { + fn new() -> Option<BatProjectDirs> { + #[cfg(target_os = "macos")] + let cache_dir_op = env::var_os("XDG_CACHE_HOME") + .map(PathBuf::from) + .filter(|p| p.is_absolute()) + .or_else(|| dirs_next::home_dir().map(|d| d.join(".cache"))); + + #[cfg(not(target_os = "macos"))] + let cache_dir_op = dirs_next::cache_dir(); + + let cache_dir = cache_dir_op.map(|d| d.join("bat"))?; + + Some(BatProjectDirs { cache_dir }) + } + + pub fn cache_dir(&self) -> &Path { + &self.cache_dir + } +} + +lazy_static! { + pub static ref PROJECT_DIRS: BatProjectDirs = + BatProjectDirs::new().unwrap_or_else(|| panic!("Could not get home directory")); +} diff --git a/src/utils/bat/less.rs b/src/utils/bat/less.rs new file mode 100644 index 00000000..1ca9f76f --- /dev/null +++ b/src/utils/bat/less.rs @@ -0,0 +1,66 @@ +use std::process::Command; + +pub fn retrieve_less_version() -> Option<usize> { + if let Ok(less_path) = grep_cli::resolve_binary("less") { + let cmd = Command::new(less_path).arg("--version").output().ok()?; + parse_less_version(&cmd.stdout) + } else { + None + } +} + +fn parse_less_version(output: &[u8]) -> Option<usize> { + if output.starts_with(b"less ") { + let version = std::str::from_utf8(&output[5..]).ok()?; + let end = version.find(|c: char| !c.is_ascii_digit())?; + version[..end].parse::<usize>().ok() + } else { + None + } +} + +#[test] +fn test_parse_less_version_487() { + let output = b"less 487 (GNU regular expressions) +Copyright (C) 1984-2016 Mark Nudelman + +less comes with NO WARRANTY, to the extent permitted by law. +For information about the terms of redistribution, +see the file named README in the less distribution. +Homepage: http://www.greenwoodsoftware.com/less"; + + assert_eq!(Some(487), parse_less_version(output)); +} + +#[test] +fn test_parse_less_version_529() { + let output = b"less 529 (Spencer V8 regular expressions) +Copyright (C) 1984-2017 Mark Nudelman + +less comes with NO WARRANTY, to the extent permitted by law. +For information about the terms of redistribution, +see the file named README in the less distribution. +Homepage: http://www.greenwoodsoftware.com/less"; + + assert_eq!(Some(529), parse_less_version(output)); +} + +#[test] +fn test_parse_less_version_551() { + let output = b"less 551 (PCRE regular expressions) +Copyright (C) 1984-2019 Mark Nudelman + +less comes with NO WARRANTY, to the extent permitted by law. +For information about the terms of redistribution, +see the file named README in the less distribution. +Home page: http://www.greenwoodsoftware.com/less"; + + assert_eq!(Some(551), parse_less_version(output)); +} + +#[test] +fn test_parse_less_version_wrong_program() { + let output = b"more from util-linux 2.34"; + + assert_eq!(None, parse_less_version(output)); +} diff --git a/src/utils/bat/mod.rs b/src/utils/bat/mod.rs new file mode 100644 index 00000000..7812e7c0 --- /dev/null +++ b/src/utils/bat/mod.rs @@ -0,0 +1,5 @@ +pub mod assets; +pub mod dirs; +mod less; +pub mod output; +pub mod terminal; diff --git a/src/utils/bat/output.rs b/src/utils/bat/output.rs new file mode 100644 index 00000000..a231168b --- /dev/null +++ b/src/utils/bat/output.rs @@ -0,0 +1,204 @@ +// https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468 +// src/output.rs +// See src/utils/bat/LICENSE +use std::env; +use std::ffi::OsString; +use std::io::{self, Write}; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; + +use super::less::retrieve_less_version; + +use crate::config; +use crate::fatal; +use crate::features::navigate; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[allow(dead_code)] +pub enum PagingMode { + Always, + QuitIfOneScreen, + Never, +} +use crate::errors::*; + +pub enum OutputType { + Pager(Child), + Stdout(io::Stdout), +} + +impl OutputType { + pub fn from_mode( + mode: PagingMode, + pager: Option<String>, + config: &config::Config, + ) -> Result<Self> { + use self::PagingMode::*; + Ok(match mode { + Always => OutputType::try_pager(false, pager, config)?, + QuitIfOneScreen => OutputType::try_pager(true, pager, config)?, + _ => OutputType::stdout(), + }) + } + + /// Try to launch the pager. Fall back to stdout in case of errors. + fn try_pager( + quit_if_one_screen: bool, + pager_from_config: Option<String>, + config: &config::Config, + ) -> Result<Self> { + let mut replace_arguments_to_less = false; + + let pager_from_env = match ( + env::var("DELTA_PAGER"), + env::var("BAT_PAGER"), + env::var("PAGER"), + ) { + (Ok(delta_pager), _, _) => Some(delta_pager), + (_, Ok(bat_pager), _) => Some(bat_pager), + (_, _, Ok(pager)) => { + // less needs to be called with the '-R' option in order to properly interpret ANSI + // color sequences. If someone has set PAGER="less -F", we therefore need to + // overwrite the arguments and add '-R'. + // We only do this for PAGER, since it is used in other contexts. + replace_arguments_to_less = true; + Some(pager) + } + _ => None, + }; + + if pager_from_config.is_some() { + replace_arguments_to_less = false; + } + + let pager = pager_from_config + .or(pager_from_env) + .unwrap_or_else(|| String::from("less")); + + let pagerflags = + shell_words::split(&pager).chain_err(|| "Could not parse pager command.")?; + + Ok(match pagerflags.split_first() { + Some((pager_name, args)) => { + let pager_path = PathBuf::from(pager_name); + + let is_less = pager_path.file_stem() == Some(&OsString::from("less")); + + let process = if is_less { + _make_process_from_less_path( + pager_path, + args, + replace_arguments_to_less, + quit_if_one_screen, + config, + ) + } else { + _make_process_from_pager_path(pager_path, args) + }; + if let Some(mut process) = process { + process + .stdin(Stdio::piped()) + .spawn() + .map(OutputType::Pager) + .unwrap_or_else(|_| OutputType::stdout()) + } else { + OutputType::stdout() + } + } + None => OutputType::stdout(), + }) + } + + fn stdout() -> Self { + OutputType::Stdout(io::stdout()) + } + + pub fn handle(&mut self) -> Result<&mut dyn Write> { + Ok(match *self { + OutputType::Pager(ref mut command) => command + .stdin + .as_mut() + .chain_err(|| "Could not open stdin for pager")?, + OutputType::Stdout(ref mut handle) => handle, + }) + } +} + +fn _make_process_from_less_path( + less_path: PathBuf, + args: &[String], + replace_arguments_to_less: bool, + quit_if_one_screen: bool, + config: &config::Config, +) -> Option<Command> { + if let Ok(less_path) = grep_cli::resolve_binary(less_path) { + let mut p = Command::new(&less_path); + if args.is_empty() || replace_arguments_to_less { + p.args(vec!["--RAW-CONTROL-CHARS"]); + + // Passing '--no-init' fixes a bug with '--quit-if-one-screen' in older + // versions of 'less'. Unfortunately, it also breaks mouse-wheel support. + // + // See: http://www.greenwoodsoftware.com/less/news.530.html + // + // For newer versions (530 or 558 on Windows), we omit '--no-init' as it + // is not needed anymore. + match retrieve_less_version() { + None => { + p.arg("--no-init"); + } + Some(version) if (version < 530 || (cfg!(windows) && version < 558)) => { + p.arg("--no-init"); + } + _ => {} + } + + if quit_if_one_screen { + p.arg("--quit-if-one-screen"); + } + } else { + p.args(args); + } + p.env("LESSCHARSET", "UTF-8"); + p.env("LESSANSIENDCHARS", "mK"); + if config.navigate { + if let Ok(hist_file) = navigate::copy_less_hist_file_and_append_navigate_regexp(config) + { + p.env("LESSHISTFILE", hist_file); + if config.show_themes { + p.arg("+n"); + } + } + } + Some(p) + } else { + None + } +} + +fn _make_process_from_pager_path(pager_path: PathBuf, args: &[String]) -> Option<Command> { + if pager_path.file_stem() == Some(&OsString::from("delta")) { + fatal( + "\ +It looks like you have set delta as the value of $PAGER. \ +This would result in a non-terminating recursion. \ +delta is not an appropriate value for $PAGER \ +(but it is an appropriate value for $GIT_PAGER).", + ); + } + if let Ok(pager_path) = grep_cli::resolve_binary(pager_path) { + let mut p = Command::new(&pager_path); + p.args(args); + Some(p) + } else { + None + } +} + +impl Drop for OutputType { + fn drop(&mut self) { + if let OutputType::Pager(ref mut command) = *self { + let _ = command.wait(); + } + } +} diff --git a/src/utils/bat/terminal.rs b/src/utils/bat/terminal.rs new file mode 100644 index 00000000..15c75d24 --- /dev/null +++ b/src/utils/bat/terminal.rs @@ -0,0 +1,83 @@ +use ansi_term::Color::{self, Fixed, RGB}; +use ansi_term::{self, Style}; + +use syntect::highlighting::{self, FontStyle}; + +pub fn to_ansi_color(color: highlighting::Color, true_color: bool) -> Option<ansi_term::Color> { + if color.a == 0 { + // Themes can specify one of the user-configurable terminal colors by + // encoding them as #RRGGBBAA with AA set to 00 (transparent) and RR set + // to the 8-bit color palette number. The built-in themes ansi, base16, + // and base16-256 use this. + Some(match color.r { + // For the first 8 colors, use the Color enum to produce ANSI escape + // sequences using codes 30-37 (foreground) and 40-47 (background). + // For example, red foreground is \x1b[31m. This works on terminals + // without 256-color support. + 0x00 => Color::Black, + 0x01 => Color::Red, + 0x02 => Color::Green, + 0x03 => Color::Yellow, + 0x04 => Color::Blue, + 0x05 => Color::Purple, + 0x06 => Color::Cyan, + 0x07 => Color::White, + // For all other colors, use Fixed to produce escape sequences using + // codes 38;5 (foreground) and 48;5 (background). For example, + // bright red foreground is \x1b[38;5;9m. This only works on + // terminals with 256-color support. + // + // TODO: When ansi_term adds support for bright variants using codes + // 90-97 (foreground) and 100-107 (background), we should use those + // for values 0x08 to 0x0f and only use Fixed for 0x10 to 0xff. + n => Fixed(n), + }) + } else if color.a == 1 { + // Themes can specify the terminal's default foreground/background color + // (i.e. no escape sequence) using the encoding #RRGGBBAA with AA set to + // 01. The built-in theme ansi uses this. + None + } else if true_color { + Some(RGB(color.r, color.g, color.b)) + } else { + Some(Fixed(ansi_colours::ansi256_from_rgb(( + color.r, color.g, color.b, + )))) + } +} + +#[allow(dead_code)] +pub fn as_terminal_escaped( + style: highlighting::Style, + text: &str, + true_color: bool, + colored: bool, + italics: bool, + background_color: Option<highlighting::Color>, +) -> String { + if text.is_empty() { + return text.to_string(); + } + + let mut style = if !colored { + Style::default() + } else { + let mut color = Style { + foreground: to_ansi_color(style.foreground, true_color), + ..Style::default() + }; + if style.font_style.contains(FontStyle::BOLD) { + color = color.bold(); + } + if style.font_style.contains(FontStyle::UNDERLINE) { + color = color.underline(); + } + if italics && style.font_style.contains(FontStyle::ITALIC) { + color = color.italic(); + } + color + }; + + style.background = background_color.and_then(|c| to_ansi_color(c, true_color)); + style.paint(text).to_string() +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 00000000..70026dd3 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,4 @@ +#[cfg(not(tarpaulin_include))] +pub mod bat; +pub mod process; +pub mod syntect; diff --git a/src/utils/process.rs b/src/utils/process.rs new file mode 100644 index 00000000..844f9f31 --- /dev/null +++ b/src/utils/process.rs @@ -0,0 +1,304 @@ +use std::collections::{HashMap, HashSet}; +use sysinfo::{Pid, Process, ProcessExt, SystemExt}; + +#[derive(Debug, PartialEq)] +pub enum GitBlameExtension { + Some(String), + None, + NotGitBlame, +} + +pub fn git_blame_filename_extension() -> Option<String> { + let mut info = sysinfo::System::new(); + let my_pid = std::process::id() as Pid; + + // 1) Try the parent process. If delta is set as the pager in git, then git is the parent process. + let parent = parent_process(&mut info, my_pid)?; + + match guess_git_blame_filename_extension(parent.cmd()) { + GitBlameExtension::Some(ext) => return Some(ext), + GitBlameExtension::None => return None, + + // 2) The parent process was something else, this can happen if git output is piped into delta, e.g. + // `git blame foo.txt | delta`. When the shell sets up the pipe it creates the two processes, the pids + // are usually consecutive, so check if the proceess with `my_pid - 1` matches. + GitBlameExtension::NotGitBlame => { + let sibling = naive_sibling_process(&mut info, my_pid); + if let Some(proc) = sibling { + if let GitBlameExtension::Some(ext) = guess_git_blame_filename_extension(proc.cmd()) + { + return Some(ext); + } + } + // else try the fallback + } + } + + /* + 3) Neither parent nor direct sibling were a match. + The most likely case is that the input program of the pipe wrote all its data and exited before delta + started, so no file extension can be retrieved. Same if the data was piped from an input file. + + There might also be intermediary scripts in between or piped input with randomized pids, so check all + processes for the closest `git blame` in the process tree. + + 100 /usr/bin/some-terminal-emulator + 124 \_ -shell + 301 | \_ /usr/bin/git blame src/main.rs + 302 | \_ wraps_delta.sh + 303 | \_ delta + 304 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen + 125 \_ -shell + 800 | \_ /usr/bin/git blame src/main.rs + 400 | \_ delta + 200 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen + 126 \_ -shell + 501 | \_ /bin/sh /wrapper/for/git blame src/main.rs + 555 | | \_ /usr/bin/git blame src/main.rs + 502 | \_ delta + 567 | \_ less --RAW-CONTROL-CHARS --quit-if-one-screen + + */ + find_sibling_process(&mut info, my_pid) +} + +// Skip all arguments starting with '-' from `args_it`. Also skip all arguments listed in +// `skip_this_plus_parameter` plus their respective next argument. +// Keep all arguments once a '--' is encountered. +// (Note that some an argument work with and without '=', e.g. '--foo' 'bar' and '--foo=bar') +fn skip_uninteresting_args<'a, 'b, ArgsI, SkipI>( + mut args_it: ArgsI, + skip_this_plus_parameter: SkipI, +) -> Vec<&'a str> +where + ArgsI: Iterator<Item = &'a str>, + SkipI: Iterator<Item = &'b str>, +{ + let arg_follows_space: HashSet<&'b str> = skip_this_plus_parameter.into_iter().collect(); + + let mut result = Vec::new(); + loop { + match args_it.next() { + None => break result, + Some("--") => { + result.extend(args_it); + break result; + } + Some(arg) if arg_follows_space.contains(arg) => { + let _skip_parameter = args_it.next(); + } + Some(arg) if !arg.starts_with('-') => { + result.push(arg); + } + Some(_) => { /* skip: --these -and --also=this */ } + } + } +} + +fn guess_git_blame_filename_extension(args: &[String]) -> GitBlameExtension { + { + let mut it = args.iter(); + match (it.next(), it.next()) { + // git blame or git -C/-c etc. and then (maybe) blame + (Some(git), Some(blame)) + if git.contains("git") && (blame == "blame" || blame.starts_with('-')) => {} + _ => return GitBlameExtension::NotGitBlame, + } + } + + let args = args.iter().skip(2).map(|s| s.as_str()); + + // See git(1) and git-blame(1). Some arguments separate their parameter with space or '=', e.g. + // --date=2015 or --date 2015. + let git_blame_options_with_parameter = + "-C -c -L --since --ignore-rev --ignore-revs-file --contents --reverse --date"; + + match skip_uninteresting_args(args, git_blame_options_with_parameter.split(' ')) + .last() + .and_then(|&s| s.split('.').last()) + .map(str::to_owned) + { + Some(ext) => GitBlameExtension::Some(ext), + None => GitBlameExtension::None, + } +} + +fn parent_process(info: &mut sysinfo::System, my_pid: Pid) -> Option<&Process> { + info.refresh_process(my_pid).then(|| ())?; + + let parent_pid = info.process(my_pid)?.parent()?; + info.refresh_process(parent_pid).then(|| ())?; + info.process(parent_pid) +} + +fn naive_sibling_process(info: &mut sysinfo::System, my_pid: Pid) -> Option<&Process> { + let sibling_pid = my_pid - 1; + info.refresh_process(sibling_pid).then(|| ())?; + info.process(sibling_pid) +} + +fn iter_parents<F>(info: &sysinfo::System, pid: Pid, distance: usize, mut f: F) +where + F: FnMut(Pid, usize), +{ + if let Some(proc) = info.process(pid) { + if let Some(pid) = proc.parent() { + f(pid, distance); + iter_parents(info, pid, distance + 1, f) + } + } +} + +fn find_sibling_process(info: &mut sysinfo::System, my_pid: Pid) -> Option<String> { + info.refresh_processes(); + + let this_sta |