forked from mirrors/jj
1f6a404646
This allows us to reconfigure ui with the parsed --color option. I tried if implementing formatter.into_output() would make sense, and it turned out to be a bit mess as the Formatter trait doesn't know the lifetime of the underlying output. Ui could own the formatter behind Color|Plain enum variant in place of Box<dyn>, but that seemed to unnecessarily change the Ui interface with little benefit. Since we just want to reinitialize the ui at very early stage, I think recreating the formatters is the simplest way to go. Regarding the formatter API, I have a feeling that Ui should keep the underlying stdout/stderr/color_map instead of the stateful formatters. ui.stdout_formatter() will return a temporary formatter, and maybe dropping it will automatically clear labels. This would also means the temporary formatter could be created with stdout.lock().
399 lines
13 KiB
Rust
399 lines
13 KiB
Rust
// Copyright 2020 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use std::io::Write;
|
|
use std::path::{Component, Path, PathBuf};
|
|
use std::str::FromStr;
|
|
use std::sync::{Mutex, MutexGuard};
|
|
use std::{fmt, io};
|
|
|
|
use atty::Stream;
|
|
use jujutsu_lib::commit::Commit;
|
|
use jujutsu_lib::op_store::WorkspaceId;
|
|
use jujutsu_lib::repo::RepoRef;
|
|
use jujutsu_lib::repo_path::{RepoPath, RepoPathComponent, RepoPathJoin};
|
|
use jujutsu_lib::settings::UserSettings;
|
|
|
|
use crate::formatter::{ColorFormatter, Formatter, PlainTextFormatter};
|
|
use crate::templater::TemplateFormatter;
|
|
|
|
pub struct Ui<'a> {
|
|
cwd: PathBuf,
|
|
color: bool,
|
|
stdout_formatter: Mutex<Box<dyn Formatter + 'a>>,
|
|
stderr_formatter: Mutex<Box<dyn Formatter + 'a>>,
|
|
settings: UserSettings,
|
|
}
|
|
|
|
fn new_formatter<'output>(
|
|
settings: &UserSettings,
|
|
color: bool,
|
|
output: Box<dyn Write + 'output>,
|
|
) -> Box<dyn Formatter + 'output> {
|
|
if color {
|
|
Box::new(ColorFormatter::new(output, settings))
|
|
} else {
|
|
Box::new(PlainTextFormatter::new(output))
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum ColorChoice {
|
|
Always,
|
|
Never,
|
|
Auto,
|
|
}
|
|
|
|
impl Default for ColorChoice {
|
|
fn default() -> Self {
|
|
ColorChoice::Auto
|
|
}
|
|
}
|
|
|
|
impl FromStr for ColorChoice {
|
|
type Err = &'static str;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"always" => Ok(ColorChoice::Always),
|
|
"never" => Ok(ColorChoice::Never),
|
|
"auto" => Ok(ColorChoice::Auto),
|
|
_ => Err("must be one of always, never, or auto"),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn color_setting(settings: &UserSettings) -> ColorChoice {
|
|
settings
|
|
.config()
|
|
.get_string("ui.color")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn use_color(choice: ColorChoice) -> bool {
|
|
match choice {
|
|
ColorChoice::Always => true,
|
|
ColorChoice::Never => false,
|
|
ColorChoice::Auto => atty::is(Stream::Stdout),
|
|
}
|
|
}
|
|
|
|
impl<'stdout> Ui<'stdout> {
|
|
pub fn new(
|
|
cwd: PathBuf,
|
|
stdout: Box<dyn Write + 'stdout>,
|
|
stderr: Box<dyn Write + 'stdout>,
|
|
color: bool,
|
|
settings: UserSettings,
|
|
) -> Ui<'stdout> {
|
|
let stdout_formatter = Mutex::new(new_formatter(&settings, color, stdout));
|
|
let stderr_formatter = Mutex::new(new_formatter(&settings, color, stderr));
|
|
Ui {
|
|
cwd,
|
|
color,
|
|
stdout_formatter,
|
|
stderr_formatter,
|
|
settings,
|
|
}
|
|
}
|
|
|
|
pub fn for_terminal(settings: UserSettings) -> Ui<'static> {
|
|
let cwd = std::env::current_dir().unwrap();
|
|
let stdout: Box<dyn Write + 'static> = Box::new(io::stdout());
|
|
let stderr: Box<dyn Write + 'static> = Box::new(io::stderr());
|
|
let color = use_color(color_setting(&settings));
|
|
Ui::new(cwd, stdout, stderr, color, settings)
|
|
}
|
|
|
|
/// Reconfigures the underlying outputs with the new color choice.
|
|
///
|
|
/// It's up to caller to ensure that the current output formatters have no
|
|
/// labels applied. Otherwise the current color would persist.
|
|
pub fn reset_color_for_terminal(&mut self, choice: ColorChoice) {
|
|
let color = use_color(choice);
|
|
if self.color != color {
|
|
// it seems uneasy to unwrap the underlying output from the formatter, so
|
|
// recreate it.
|
|
let stdout_formatter = new_formatter(&self.settings, color, Box::new(io::stdout()));
|
|
let stderr_formatter = new_formatter(&self.settings, color, Box::new(io::stderr()));
|
|
self.color = color;
|
|
*self.stdout_formatter.get_mut().unwrap() = stdout_formatter;
|
|
*self.stderr_formatter.get_mut().unwrap() = stderr_formatter;
|
|
}
|
|
}
|
|
|
|
pub fn cwd(&self) -> &Path {
|
|
&self.cwd
|
|
}
|
|
|
|
pub fn settings(&self) -> &UserSettings {
|
|
&self.settings
|
|
}
|
|
|
|
pub fn new_formatter<'output>(
|
|
&self,
|
|
output: Box<dyn Write + 'output>,
|
|
) -> Box<dyn Formatter + 'output> {
|
|
new_formatter(&self.settings, self.color, output)
|
|
}
|
|
|
|
pub fn stdout_formatter(&self) -> MutexGuard<Box<dyn Formatter + 'stdout>> {
|
|
self.stdout_formatter.lock().unwrap()
|
|
}
|
|
|
|
pub fn stderr_formatter(&self) -> MutexGuard<Box<dyn Formatter + 'stdout>> {
|
|
self.stderr_formatter.lock().unwrap()
|
|
}
|
|
|
|
pub fn write(&mut self, text: &str) -> io::Result<()> {
|
|
self.stdout_formatter().write_str(text)
|
|
}
|
|
|
|
pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
|
|
self.stdout_formatter().write_fmt(fmt)
|
|
}
|
|
|
|
pub fn write_hint(&mut self, text: impl AsRef<str>) -> io::Result<()> {
|
|
let mut formatter = self.stderr_formatter();
|
|
formatter.add_label(String::from("hint"))?;
|
|
formatter.write_str(text.as_ref())?;
|
|
formatter.remove_label()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn write_warn(&mut self, text: impl AsRef<str>) -> io::Result<()> {
|
|
let mut formatter = self.stderr_formatter();
|
|
formatter.add_label(String::from("warning"))?;
|
|
formatter.write_str(text.as_ref())?;
|
|
formatter.remove_label()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn write_error(&mut self, text: &str) -> io::Result<()> {
|
|
let mut formatter = self.stderr_formatter();
|
|
formatter.add_label(String::from("error"))?;
|
|
formatter.write_str(text)?;
|
|
formatter.remove_label()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn write_commit_summary(
|
|
&mut self,
|
|
repo: RepoRef,
|
|
workspace_id: &WorkspaceId,
|
|
commit: &Commit,
|
|
) -> io::Result<()> {
|
|
let template_string = self
|
|
.settings
|
|
.config()
|
|
.get_string("template.commit_summary")
|
|
.unwrap_or_else(|_| {
|
|
String::from(
|
|
r#"label(if(open, "open"), commit_id.short() " " description.first_line())"#,
|
|
)
|
|
});
|
|
let template =
|
|
crate::template_parser::parse_commit_template(repo, workspace_id, &template_string);
|
|
let mut formatter = self.stdout_formatter();
|
|
let mut template_writer = TemplateFormatter::new(template, formatter.as_mut());
|
|
template_writer.format(commit)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Parses a path relative to cwd into a RepoPath relative to wc_path
|
|
pub fn parse_file_path(
|
|
&self,
|
|
wc_path: &Path,
|
|
input: &str,
|
|
) -> Result<RepoPath, FilePathParseError> {
|
|
let repo_relative_path = relative_path(wc_path, &self.cwd.join(input));
|
|
let mut repo_path = RepoPath::root();
|
|
for component in repo_relative_path.components() {
|
|
match component {
|
|
Component::Normal(a) => {
|
|
repo_path = repo_path.join(&RepoPathComponent::from(a.to_str().unwrap()));
|
|
}
|
|
Component::CurDir => {}
|
|
Component::ParentDir => {
|
|
if let Some(parent) = repo_path.parent() {
|
|
repo_path = parent;
|
|
} else {
|
|
return Err(FilePathParseError::InputNotInRepo(input.to_string()));
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(FilePathParseError::InputNotInRepo(input.to_string()));
|
|
}
|
|
}
|
|
}
|
|
Ok(repo_path)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
|
pub enum FilePathParseError {
|
|
InputNotInRepo(String),
|
|
}
|
|
|
|
pub fn relative_path(mut from: &Path, to: &Path) -> PathBuf {
|
|
let mut result = PathBuf::from("");
|
|
loop {
|
|
if let Ok(suffix) = to.strip_prefix(from) {
|
|
result = result.join(suffix);
|
|
break;
|
|
}
|
|
if let Some(parent) = from.parent() {
|
|
result = result.join("..");
|
|
from = parent;
|
|
} else {
|
|
result = to.to_path_buf();
|
|
break;
|
|
}
|
|
}
|
|
if result.as_os_str().is_empty() {
|
|
result = PathBuf::from(".");
|
|
}
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::io::Cursor;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_file_path_wc_in_cwd() {
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
let cwd_path = temp_dir.path().join("repo");
|
|
let wc_path = cwd_path.clone();
|
|
let mut unused_stdout_buf = vec![];
|
|
let mut unused_stderr_buf = vec![];
|
|
let unused_stdout = Box::new(Cursor::new(&mut unused_stdout_buf));
|
|
let unused_stderr = Box::new(Cursor::new(&mut unused_stderr_buf));
|
|
let ui = Ui::new(
|
|
cwd_path,
|
|
unused_stdout,
|
|
unused_stderr,
|
|
false,
|
|
UserSettings::default(),
|
|
);
|
|
|
|
assert_eq!(ui.parse_file_path(&wc_path, ""), Ok(RepoPath::root()));
|
|
assert_eq!(ui.parse_file_path(&wc_path, "."), Ok(RepoPath::root()));
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "file"),
|
|
Ok(RepoPath::from_internal_string("file"))
|
|
);
|
|
// Both slash and the platform's separator are allowed
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, &format!("dir{}file", std::path::MAIN_SEPARATOR)),
|
|
Ok(RepoPath::from_internal_string("dir/file"))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "dir/file"),
|
|
Ok(RepoPath::from_internal_string("dir/file"))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, ".."),
|
|
Err(FilePathParseError::InputNotInRepo("..".to_string()))
|
|
);
|
|
// TODO: handle these cases:
|
|
// assert_eq!(ui.parse_file_path(&cwd_path, "../repo"),
|
|
// Ok(RepoPath::root())); assert_eq!(ui.parse_file_path(&cwd_path,
|
|
// "../repo/file"), Ok(RepoPath::from_internal_string("file")));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_file_path_wc_in_cwd_parent() {
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
let cwd_path = temp_dir.path().join("dir");
|
|
let wc_path = cwd_path.parent().unwrap().to_path_buf();
|
|
let mut unused_stdout_buf = vec![];
|
|
let mut unused_stderr_buf = vec![];
|
|
let unused_stdout = Box::new(Cursor::new(&mut unused_stdout_buf));
|
|
let unused_stderr = Box::new(Cursor::new(&mut unused_stderr_buf));
|
|
let ui = Ui::new(
|
|
cwd_path,
|
|
unused_stdout,
|
|
unused_stderr,
|
|
false,
|
|
UserSettings::default(),
|
|
);
|
|
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, ""),
|
|
Ok(RepoPath::from_internal_string("dir"))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "."),
|
|
Ok(RepoPath::from_internal_string("dir"))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "file"),
|
|
Ok(RepoPath::from_internal_string("dir/file"))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "subdir/file"),
|
|
Ok(RepoPath::from_internal_string("dir/subdir/file"))
|
|
);
|
|
assert_eq!(ui.parse_file_path(&wc_path, ".."), Ok(RepoPath::root()));
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "../.."),
|
|
Err(FilePathParseError::InputNotInRepo("../..".to_string()))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "../other-dir/file"),
|
|
Ok(RepoPath::from_internal_string("other-dir/file"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_file_path_wc_in_cwd_child() {
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
let cwd_path = temp_dir.path().join("cwd");
|
|
let wc_path = cwd_path.join("repo");
|
|
let mut unused_stdout_buf = vec![];
|
|
let mut unused_stderr_buf = vec![];
|
|
let unused_stdout = Box::new(Cursor::new(&mut unused_stdout_buf));
|
|
let unused_stderr = Box::new(Cursor::new(&mut unused_stderr_buf));
|
|
let ui = Ui::new(
|
|
cwd_path,
|
|
unused_stdout,
|
|
unused_stderr,
|
|
false,
|
|
UserSettings::default(),
|
|
);
|
|
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, ""),
|
|
Err(FilePathParseError::InputNotInRepo("".to_string()))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "not-repo"),
|
|
Err(FilePathParseError::InputNotInRepo("not-repo".to_string()))
|
|
);
|
|
assert_eq!(ui.parse_file_path(&wc_path, "repo"), Ok(RepoPath::root()));
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "repo/file"),
|
|
Ok(RepoPath::from_internal_string("file"))
|
|
);
|
|
assert_eq!(
|
|
ui.parse_file_path(&wc_path, "repo/dir/file"),
|
|
Ok(RepoPath::from_internal_string("dir/file"))
|
|
);
|
|
}
|
|
}
|