Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions src/git/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Module for git integration.
use git2::{Repository, StatusOptions};
use status::StatusGetter;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use util::StatusEntryExt;
Expand Down Expand Up @@ -67,20 +68,13 @@ impl Git {
options
}

/// Gets the tracked status for a file.
pub fn tracked_status<P>(&self, path: P) -> Result<Option<status::Tracked>, git2::Error>
/// Gets the status for a file.
pub fn status<S, P>(&self, path: P) -> Result<Option<status::Status>, git2::Error>
where
S: StatusGetter,
P: AsRef<Path>,
{
self.git2_status(path).map(status::Tracked::from_git2)
}

/// Gets the untracked status for a file.
pub fn untracked_status<P>(&self, path: P) -> Result<Option<status::Untracked>, git2::Error>
where
P: AsRef<Path>,
{
self.git2_status(path).map(status::Untracked::from_git2)
self.git2_status(path).map(S::from_git2)
}

/// Gets the original gt2 status for a file.
Expand Down
116 changes: 54 additions & 62 deletions src/git/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,6 @@

use mlua::{IntoLua, Lua};

/// The tracked git status.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tracked(Status);

impl Tracked {
/// Gets the index status from the git2 status.
pub fn from_git2(status: git2::Status) -> Option<Self> {
use Status::*;

let kind = if status.is_index_new() {
Added
} else if status.is_index_modified() {
Modified
} else if status.is_index_deleted() {
Removed
} else if status.is_index_renamed() {
Renamed
} else {
return None;
};

Some(Self(kind))
}

/// Gets the backing status enum.
pub fn status(&self) -> Status {
self.0
}
}

/// The untracked git status.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Untracked(Status);

impl Untracked {
/// Gets the worktree status from the git2 status.
pub fn from_git2(status: git2::Status) -> Option<Self> {
use Status::*;

let kind = if status.is_wt_new() {
Added
} else if status.is_wt_modified() {
Modified
} else if status.is_wt_deleted() {
Removed
} else if status.is_wt_renamed() {
Renamed
} else {
return None;
};

Some(Self(kind))
}

/// Gets the backing status enum.
pub fn status(&self) -> Status {
self.0
}
}

/// Git statuses (tracked/indexed or untracked/worktree) for a file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
Expand Down Expand Up @@ -102,6 +42,60 @@ impl IntoLua for Status {
}
}

/// Trait to generalize getting a git status.
pub trait StatusGetter {
/// Gets the status from a git2 status.
fn from_git2(status: git2::Status) -> Option<Status>;
}

/// The tracked git status.
pub struct Tracked;

impl StatusGetter for Tracked {
/// Gets the index status from the git2 status.
fn from_git2(status: git2::Status) -> Option<Status> {
use Status::*;

let status = if status.is_index_new() {
Added
} else if status.is_index_modified() {
Modified
} else if status.is_index_deleted() {
Removed
} else if status.is_index_renamed() {
Renamed
} else {
return None;
};

Some(status)
}
}

/// The untracked git status.
pub struct Untracked;

impl StatusGetter for Untracked {
/// Gets the worktree status from the git2 status.
fn from_git2(status: git2::Status) -> Option<Status> {
use Status::*;

let status = if status.is_wt_new() {
Added
} else if status.is_wt_modified() {
Modified
} else if status.is_wt_deleted() {
Removed
} else if status.is_wt_renamed() {
Renamed
} else {
return None;
};

Some(status)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -116,7 +110,6 @@ mod tests {
#[case(Libgit::INDEX_RENAMED, Some(Renamed))]
#[case(Libgit::WT_NEW, None)]
fn test_tracked_from_git2(#[case] libgit: Libgit, #[case] expected: Option<Status>) {
let expected = expected.map(Tracked);
assert_eq!(expected, Tracked::from_git2(libgit));
}

Expand All @@ -127,7 +120,6 @@ mod tests {
#[case(Libgit::WT_RENAMED, Some(Renamed))]
#[case(Libgit::INDEX_NEW, None)]
fn test_untracked_from_git2(#[case] libgit: Libgit, #[case] expected: Option<Status>) {
let expected = expected.map(Untracked);
assert_eq!(expected, Untracked::from_git2(libgit));
}
}
165 changes: 83 additions & 82 deletions src/tree/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Provides the utility for generating a tree.
use crate::color::{Color, ColorChoice};
use crate::config;
use crate::git::status;
use crate::git::{Git, status::Status};
pub use builder::Builder;
pub use charset::Charset;
Expand Down Expand Up @@ -333,100 +334,35 @@ where
where
W: Write,
{
const NO_STATUS: &str = " ";

let Some(git) = self.git else { return Ok(()) };

// HACK cached status keys don't have a ./ prefix and git2 apparently doesn't expect it.
let path = self
.clean_path_for_git2(path)
.expect("Should be able to resolve path relative to git root");

// TODO This is repetitive. Refactor.
let untracked_status = git
.untracked_status(&path)
.ok()
.flatten()
.map(|untracked| untracked.status());
let untracked_color =
untracked_status.and_then(|status| self.get_untracked_git_status_color(status));
let untracked_status = untracked_status
.map(|status| status.as_str())
.unwrap_or(NO_STATUS);
let tracked_status = git
.tracked_status(path)
.ok()
.flatten()
.map(|tracked| tracked.status());
let tracked_color =
tracked_status.and_then(|status| self.get_tracked_git_status_color(status));
let tracked_status = tracked_status
.map(|status| status.as_str())
.unwrap_or(NO_STATUS);

self.color_choice
.write_to(writer, untracked_status, untracked_color, None)?;
self.color_choice
.write_to(writer, tracked_status, tracked_color, None)?;
self.write_status::<status::Untracked, _, _>(writer, git, &path)?;
self.write_status::<status::Tracked, _, _>(writer, git, path)?;
Ok(())
}

/// Gets the tracked git status color.
fn get_tracked_git_status_color(&self, status: Status) -> Option<Color> {
use Status::*;
use owo_colors::AnsiColors::{self, *};

const DEFAULT_ADDED: AnsiColors = Green;
const DEFAULT_MODIFIED: AnsiColors = Yellow;
const DEFAULT_REMOVED: AnsiColors = Red;
const DEFAULT_RENAMED: AnsiColors = Cyan;

let default_color = match status {
Added => DEFAULT_ADDED,
Modified => DEFAULT_MODIFIED,
Removed => DEFAULT_REMOVED,
Renamed => DEFAULT_RENAMED,
};
let default_color = Color::Ansi(default_color);
let default_color = Some(default_color);

self.colors
.as_ref()
.and_then(|config| {
config
.for_untracked_git_status(status, default_color)
.expect("Config should return a valid color")
})
.or(default_color)
}

/// Gets the untracked git status color.
fn get_untracked_git_status_color(&self, status: Status) -> Option<Color> {
use Status::*;
use owo_colors::AnsiColors::{self, *};

const DEFAULT_ADDED: AnsiColors = BrightGreen;
const DEFAULT_MODIFIED: AnsiColors = BrightYellow;
const DEFAULT_REMOVED: AnsiColors = BrightRed;
const DEFAULT_RENAMED: AnsiColors = BrightCyan;

let default_color = match status {
Added => DEFAULT_ADDED,
Modified => DEFAULT_MODIFIED,
Removed => DEFAULT_REMOVED,
Renamed => DEFAULT_RENAMED,
};
let default_color = Color::Ansi(default_color);
let default_color = Some(default_color);
/// Writes a colorized git status.
fn write_status<S, W, P2>(&self, writer: &mut W, git: &Git, path: P2) -> io::Result<()>
where
S: status::StatusGetter + StatusColor,
W: Write,
P2: AsRef<Path>,
{
const NO_STATUS: &str = " ";

self.colors
.as_ref()
.and_then(|config| {
config
.for_tracked_git_status(status, default_color)
.expect("Config should return a valid color")
let status = git.status::<S, _>(path).ok().flatten();
let color = status.and_then(|status| {
self.colors.as_ref().and_then(|config| {
S::get_git_status_color(status, config).expect("Config should return a valid color")
})
.or(default_color)
});
let status = status.map(|status| status.as_str()).unwrap_or(NO_STATUS);
self.color_choice.write_to(writer, status, color, None)
}

/// Strips the root path prefix, which is necessary for git tools.
Expand All @@ -445,3 +381,68 @@ where
Some(path.to_path_buf())
}
}

/// Private trait to generalize getting the color for a status.
trait StatusColor {
/// Default color for added status.
const DEFAULT_ADDED: AnsiColors;
/// Default color for modified status.
const DEFAULT_MODIFIED: AnsiColors;
/// Default color for removed status.
const DEFAULT_REMOVED: AnsiColors;
/// Default color for renamed status.
const DEFAULT_RENAMED: AnsiColors;

/// Gets the default color for a status.
fn get_default_color(status: Status) -> Option<Color> {
use Status::*;

let default_color = match status {
Added => Self::DEFAULT_ADDED,
Modified => Self::DEFAULT_MODIFIED,
Removed => Self::DEFAULT_REMOVED,
Renamed => Self::DEFAULT_RENAMED,
};

let default_color = Color::Ansi(default_color);
Some(default_color)
}

/// Gets the color for a git status.
fn get_git_status_color(
status: Status,
color_config: &config::Colors,
) -> mlua::Result<Option<Color>>;
}

impl StatusColor for status::Tracked {
const DEFAULT_ADDED: AnsiColors = AnsiColors::Green;
const DEFAULT_MODIFIED: AnsiColors = AnsiColors::Yellow;
const DEFAULT_REMOVED: AnsiColors = AnsiColors::Red;
const DEFAULT_RENAMED: AnsiColors = AnsiColors::Cyan;

/// Gets the tracked git status color.
fn get_git_status_color(
status: Status,
color_config: &config::Colors,
) -> mlua::Result<Option<Color>> {
let default_choice = Self::get_default_color(status);
color_config.for_tracked_git_status(status, default_choice)
}
}

impl StatusColor for status::Untracked {
const DEFAULT_ADDED: AnsiColors = AnsiColors::BrightGreen;
const DEFAULT_MODIFIED: AnsiColors = AnsiColors::BrightYellow;
const DEFAULT_REMOVED: AnsiColors = AnsiColors::BrightRed;
const DEFAULT_RENAMED: AnsiColors = AnsiColors::BrightCyan;

/// Gets the untracked git status color.
fn get_git_status_color(
status: Status,
color_config: &config::Colors,
) -> mlua::Result<Option<Color>> {
let default_choice = Self::get_default_color(status);
color_config.for_untracked_git_status(status, default_choice)
}
}