use getch;
use libpijul::patch::{Change, ChangeContext, Patch, PatchHeader, Record};
use std::io::prelude::*;
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::io::stdout;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use regex::Regex;
use commands::pretty_repo_path;
use libpijul::fs_representation::{RepoPath, RepoRoot, PIJUL_DIR_NAME};
use libpijul::status::ChangeType;
use atty;
use error::Error;
use libpijul::{EdgeFlags, Hash, LineId, MutTxn, PatchId};
use rand;
use std;
use std::char::from_u32;
use std::fs::{remove_file, File};
use std::process;
use std::str;
use term;
use term::{Attr, StdoutTerminal};
use ignore::gitignore::GitignoreBuilder;
use line;
const BINARY_CONTENTS: &'static str = "<binary contents>";
#[derive(Clone, Copy)]
pub enum Command {
Pull,
Push,
Unrecord,
}
impl Command {
fn verb(&self) -> &'static str {
match *self {
Command::Push => "push",
Command::Pull => "pull",
Command::Unrecord => "unrecord",
}
}
}
fn print_section(term: &mut Option<Box<StdoutTerminal>>, title: &str, contents: &str) {
if let Some(ref mut term) = *term {
term.attr(Attr::Bold).unwrap_or(());
}
let mut stdout = std::io::stdout();
write!(stdout, "{}", title).unwrap_or(());
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
writeln!(stdout, "{}", contents).unwrap_or(());
}
fn remove_escape_codes(f: &str) -> std::borrow::Cow<str> {
if f.as_bytes().contains(&27) {
std::borrow::Cow::Owned(f.chars().filter(|&c| c != 27 as char).collect())
} else {
std::borrow::Cow::Borrowed(f)
}
}
pub fn print_patch_descr(
term: &mut Option<Box<StdoutTerminal>>,
hash: &Hash,
internal: Option<PatchId>,
patch: &PatchHeader,
) {
print_section(term, "Hash:", &format!(" {}", &hash.to_base58()));
if let Some(internal) = internal {
print_section(term, "Internal id:", &format!(" {}", &internal.to_base58()));
}
print_section(
term,
"Authors:",
&format!(" {}", remove_escape_codes(&patch.authors.join(", "))),
);
print_section(term, "Timestamp:", &format!(" {}", patch.timestamp));
let is_tag = if !patch.flag.is_empty() { "TAG: " } else { "" };
let mut stdout = std::io::stdout();
writeln!(
stdout,
"\n {}{}",
is_tag,
remove_escape_codes(&patch.name)
)
.unwrap_or(());
if let Some(ref d) = patch.description {
writeln!(stdout, "").unwrap_or(());
let d = remove_escape_codes(d);
for descr_line in d.lines() {
writeln!(stdout, " {}", descr_line).unwrap_or(());
}
}
writeln!(stdout, "").unwrap_or(());
}
fn check_forced_decision(
command: Command,
choices: &HashMap<&Hash, bool>,
rev_dependencies: &HashMap<&Hash, Vec<&Hash>>,
a: &Hash,
b: &Patch,
) -> Option<bool> {
let covariant = match command {
Command::Pull | Command::Push => true,
Command::Unrecord => false,
};
if let Some(x) = rev_dependencies.get(a) {
for y in x {
if let Some(&choice) = choices.get(y) {
if choice == covariant {
return Some(covariant);
}
}
}
};
for y in b.dependencies().iter() {
if let Some(&choice) = choices.get(&y) {
if choice != covariant {
return Some(!covariant);
}
}
}
None
}
fn interactive_ask(
getch: &getch::Getch,
a: &Hash,
patchid: Option<PatchId>,
b: &Patch,
command_name: Command,
show_help: bool,
) -> Result<(char, Option<bool>), Error> {
let mut term = if atty::is(atty::Stream::Stdout) {
term::stdout()
} else {
None
};
print_patch_descr(&mut term, a, patchid, b);
if show_help {
display_help(command_name);
print!("Shall I {} this patch? ", command_name.verb());
} else {
print!("Shall I {} this patch? [ynkad?] ", command_name.verb());
}
stdout().flush()?;
match getch.getch().ok().and_then(|x| from_u32(x as u32)) {
Some(e) => {
println!("{}", e);
let e = e.to_uppercase().next().unwrap_or('\0');
match e {
'A' => Ok(('Y', Some(true))),
'D' => Ok(('N', Some(false))),
e => Ok((e, None)),
}
}
_ => Ok(('\0', None)),
}
}
fn display_help(c: Command) {
println!("Available options: ynkad?");
println!("y: {} this patch", c.verb());
println!("n: don't {} this patch", c.verb());
println!("k: go bacK to the previous patch");
println!("a: {} all remaining patches", c.verb());
println!("d: finish, skipping all remaining patches");
println!("")
}
pub fn ask_patches(
command: Command,
patches: &[(Hash, Option<PatchId>, Patch)],
) -> Result<Vec<Hash>, Error> {
let getch = getch::Getch::new();
let mut i = 0;
let mut choices: HashMap<&Hash, bool> = HashMap::new();
let mut rev_dependencies: HashMap<&Hash, Vec<&Hash>> = HashMap::new();
let mut final_decision = None;
let mut show_help = false;
while i < patches.len() {
let (ref a, patchid, ref b) = patches[i];
let forced_decision = check_forced_decision(command, &choices, &rev_dependencies, a, b);
let e = match final_decision.or(forced_decision) {
Some(true) => 'Y',
Some(false) => 'N',
None => {
debug!("decision not forced");
let (current, remaining) =
interactive_ask(&getch, a, patchid, b, command, show_help)?;
final_decision = remaining;
current
}
};
show_help = false;
debug!("decision: {:?}", e);
match e {
'Y' => {
choices.insert(a, true);
match command {
Command::Pull | Command::Push => {
for ref dep in b.dependencies().iter() {
let d = rev_dependencies.entry(dep).or_insert(vec![]);
d.push(a)
}
}
Command::Unrecord => {}
}
i += 1
}
'N' => {
choices.insert(a, false);
match command {
Command::Unrecord => {
for ref dep in b.dependencies().iter() {
let d = rev_dependencies.entry(dep).or_insert(vec![]);
d.push(a)
}
}
Command::Pull | Command::Push => {}
}
i += 1
}
'K' if i > 0 => {
let (ref a, _, _) = patches[i];
choices.remove(a);
i -= 1
}
'?' => {
show_help = true;
}
_ => {}
}
}
Ok(patches
.into_iter()
.filter_map(|&(ref hash, _, _)| {
if let Some(true) = choices.get(hash) {
Some(hash.to_owned())
} else {
None
}
})
.collect())
}
fn change_deps(
id: usize,
c: &Record<ChangeContext<Hash>>,
provided_by: &mut HashMap<LineId, usize>,
) -> HashSet<LineId> {
let mut s = HashSet::new();
for c in c.iter() {
match *c {
Change::NewNodes {
ref up_context,
ref down_context,
ref line_num,
ref nodes,
..
} => {
for cont in up_context.iter().chain(down_context) {
if cont.patch.is_none() && !cont.line.is_root() {
s.insert(cont.line.clone());
}
}
for i in 0..nodes.len() {
provided_by.insert(*line_num + i, id);
}
}
Change::NewEdges { ref edges, .. } => {
for e in edges {
if e.from.patch.is_none() && !e.from.line.is_root() {
s.insert(e.from.line.clone());
}
if e.to.patch.is_none() && !e.from.line.is_root() {
s.insert(e.to.line.clone());
}
}
}
}
}
s
}
fn print_record<T: rand::Rng, H>(
repo_root: &RepoRoot<impl AsRef<Path>>,
term: &mut Option<Box<StdoutTerminal>>,
cwd: &Path,
repo: &MutTxn<T>,
current_file: &mut Option<Rc<RepoPath<PathBuf>>>,
c: &Record<ChangeContext<H>>,
) {
match *c {
Record::FileAdd {
ref name,
ref contents,
..
} => {
if let Some(ref mut term) = *term {
term.fg(term::color::CYAN).unwrap_or(());
}
print!("added file ");
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
println!("{}", pretty_repo_path(repo_root, name, cwd).display());
if let Some(ref change) = contents {
print_change(term, repo, 0, 0, change);
}
}
Record::FileDel {
ref name,
ref contents,
..
} => {
if let Some(ref mut term) = *term {
term.fg(term::color::MAGENTA).unwrap_or(());
}
print!("deleted file: ");
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
println!("{}", pretty_repo_path(repo_root, name, cwd).display());
if let Some(ref change) = contents {
print_change(term, repo, 0, 0, change);
}
}
Record::FileMove { ref new_name, .. } => {
if let Some(ref mut term) = *term {
term.fg(term::color::YELLOW).unwrap_or(());
}
print!("file moved to: ");
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
println!("{}", pretty_repo_path(repo_root, new_name, cwd).display());
}
Record::Change {
ref change,
ref replacement,
ref file,
old_line,
new_line,
..
} => {
let mut file_changed = true;
if let Some(ref cur_file) = *current_file {
if file == cur_file {
file_changed = false;
}
}
if file_changed {
if let Some(ref mut term) = *term {
term.attr(Attr::Bold).unwrap_or(());
term.attr(Attr::Underline(true)).unwrap_or(());
}
println!("{}", pretty_repo_path(repo_root, file, cwd).display());
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
*current_file = Some(file.clone())
}
print_change(term, repo, old_line, new_line, change);
if let Some(ref c) = *replacement {
print_change(term, repo, old_line, new_line, c)
}
}
}
}
fn print_change<T: rand::Rng, H>(
term: &mut Option<Box<StdoutTerminal>>,
repo: &MutTxn<T>,
old_line: usize,
new_line: usize,
change: &Change<ChangeContext<H>>,
) {
match *change {
Change::NewNodes {
ref flag,
ref nodes,
..
} => {
if flag.contains(EdgeFlags::FOLDER_EDGE) {
for n in nodes {
if n.len() >= 2 {
if let Some(ref mut term) = *term {
term.fg(term::color::CYAN).unwrap_or(());
}
print!("new file ");
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
println!("{}", str::from_utf8(&n[2..]).unwrap_or(""));
}
}
} else {
if new_line > 0 {
println!("From line {}\n", new_line);
}
for n in nodes {
let s = str::from_utf8(n).unwrap_or(BINARY_CONTENTS);
if let Some(ref mut term) = *term {
term.fg(term::color::GREEN).unwrap_or(());
}
print!("+ ");
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
if s.ends_with("\n") {
print!("{}", s);
} else {
println!("{}", s);
}
}
}
}
Change::NewEdges {
ref edges, flag, ..
} => {
let mut h_targets = HashSet::with_capacity(edges.len());
if old_line > 0 {
println!("From line {}\n", old_line);
}
for e in edges {
let target = if !flag.contains(EdgeFlags::PARENT_EDGE) {
if h_targets.insert(&e.to) {
Some(&e.to)
} else {
None
}
} else {
if h_targets.insert(&e.from) {
Some(&e.from)
} else {
None
}
};
if let Some(target) = target {
let internal = repo.internal_key_unwrap(target);
let l = repo.get_contents(internal).unwrap();
let l = l.into_cow();
let s = str::from_utf8(&l).unwrap_or(BINARY_CONTENTS);
if flag.contains(EdgeFlags::DELETED_EDGE) {
if let Some(ref mut term) = *term {
term.fg(term::color::RED).unwrap_or(());
}
print!("- ");
} else {
if let Some(ref mut term) = *term {
term.fg(term::color::GREEN).unwrap_or(());
}
print!("+ ");
}
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
if s.ends_with("\n") {
print!("{}", s)
} else {
println!("{}", s)
}
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum ChangesDirection {
Record,
Revert,
}
impl ChangesDirection {
fn is_record(&self) -> bool {
match *self {
ChangesDirection::Record => true,
_ => false,
}
}
fn verb(&self) -> &str {
match *self {
ChangesDirection::Record => "record",
ChangesDirection::Revert => "revert",
}
}
}
fn display_help_changes(
potential_new_ignore: Option<&RepoPath<impl AsRef<Path>>>,
direction: ChangesDirection,
) {
println!("Available options:");
println!("y: {} this change", direction.verb());
println!("n: don't {} this change", direction.verb());
println!(
"f: {} the rest of the changes to this file",
direction.verb()
);
println!(
"s: don't {} the rest of the changes to this file",
direction.verb()
);
println!("k: go back to the previous change");
println!("a: {} all remaining changes", direction.verb());
println!("d: skip all remaining changes");
match potential_new_ignore {
Some(filename) => println!("i: ignore file {}", filename.display()),
None => (),
}
println!("")
}
fn prompt_one_change<T: rand::Rng>(
repository: &MutTxn<T>,
repo_root: &RepoRoot<impl AsRef<Path>>,
change: &Record<ChangeContext<Hash>>,
current_file: &mut Option<Rc<RepoPath<PathBuf>>>,
n_changes: usize,
i: usize,
direction: ChangesDirection,
potential_new_ignore: Option<&RepoPath<impl AsRef<Path>>>,
terminal: &mut Option<Box<StdoutTerminal>>,
getch: &getch::Getch,
cwd: &Path,
show_help: bool,
) -> Result<(char, Option<char>, Option<char>), Error> {
debug!("changes: {:?}", change);
print_record(repo_root, terminal, cwd, repository, current_file, &change);
println!("");
let choices = if potential_new_ignore.is_some() {
"[ynsfkadi?]"
} else {
"[ynsfkad?]"
};
if show_help {
display_help_changes(potential_new_ignore, direction);
print!(
"Shall I {} this change? ({}/{}) ",
direction.verb(),
i + 1,
n_changes
);
} else {
print!(
"Shall I {} this change? ({}/{}) {} ",
direction.verb(),
i + 1,
n_changes,
choices
);
}
stdout().flush()?;
match getch.getch().ok().and_then(|x| from_u32(x as u32)) {
Some(e) => {
println!("{}\n", e);
let e = e.to_uppercase().next().unwrap_or('\0');
match e {
'A' => Ok(('Y', Some('Y'), None)),
'D' => Ok(('N', Some('N'), None)),
'F' => Ok(('Y', None, Some('Y'))),
'S' => Ok(('N', None, Some('N'))),
e => Ok((e, None, None)),
}
}
_ => Ok(('\0', None, None)),
}
}
fn add_to_ignore_file(
file: &RepoPath<impl AsRef<Path>>,
repo_root: &RepoRoot<impl AsRef<Path>>,
new_ignored_patterns: &mut Vec<String>,
new_ignore_builder: &mut GitignoreBuilder,
) {
loop {
let pat = read_line_with_suggestion(
"Pattern to add to ignore file (relative to repository root, empty to add nothing)? ",
&file.as_path().to_string_lossy(),
);
if pat.is_empty() {
return;
};
let mut ignore_builder = GitignoreBuilder::new(&repo_root.repo_root);
let add_ok = match ignore_builder.add_line(None, &pat) {
Ok(i) => match i.build() {
Ok(i) => i
.matched_path_or_any_parents(file.as_path(), false)
.is_ignore(),
Err(e) => {
println!("could not match pattern {}: {}", &pat, e);
false
}
},
Err(e) => {
println!("did not understand pattern {}: {}", &pat, e);
false
}
};
if add_ok {
new_ignore_builder.add_line(None, &pat).unwrap();
new_ignored_patterns.push(pat);
return;
}
println!(
"pattern {} is incorrect or does not match {}",
pat,
&file.display()
);
}
}
pub fn ask_changes<T: rand::Rng>(
repository: &MutTxn<T>,
repo_root: &RepoRoot<impl AsRef<Path>>,
cwd: &Path,
changes: &[Record<ChangeContext<Hash>>],
direction: ChangesDirection,
to_unadd: &mut HashSet<RepoPath<PathBuf>>,
) -> Result<(HashMap<usize, bool>, Vec<String>), Error> {
debug!("changes: {:?}", changes);
let mut terminal = if atty::is(atty::Stream::Stdout) {
term::stdout()
} else {
None
};
let getch = getch::Getch::new();
let mut i = 0;
let mut choices: HashMap<usize, bool> = HashMap::new();
let mut new_ignored_patterns: Vec<String> = Vec::new();
let mut new_ignore_builder = GitignoreBuilder::new(&repo_root.repo_root);
let mut final_decision = None;
let mut file_decision: Option<char> = None;
let mut provided_by = HashMap::new();
let mut line_deps = Vec::with_capacity(changes.len());
for i in 0..changes.len() {
line_deps.push(change_deps(i, &changes[i], &mut provided_by));
}
let mut deps: HashMap<usize, Vec<usize>> = HashMap::new();
let mut rev_deps: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..changes.len() {
for dep in line_deps[i].iter() {
debug!("provided: i {}, dep {:?}", i, dep);
let p = provided_by.get(dep).unwrap();
debug!("provided: p= {}", p);
let e = deps.entry(i).or_insert(Vec::new());
e.push(*p);
let e = rev_deps.entry(*p).or_insert(Vec::new());
e.push(i);
}
}
let empty_deps = Vec::new();
let mut current_file = None;
let mut show_help = false;
while i < changes.len() {
let decision=
if deps.get(&i)
.unwrap_or(&empty_deps)
.iter()
.any(|x| { ! *(choices.get(x).unwrap_or(&true)) }) {
Some(false)
} else if rev_deps.get(&i).unwrap_or(&empty_deps)
.iter().any(|x| { *(choices.get(x).unwrap_or(&false)) }) {
Some(true)
} else {
None
};
let decision = match changes[i] {
Record::FileAdd { ref name, .. } => {
let i = new_ignore_builder.build().unwrap();
if i.matched_path_or_any_parents(name.as_path(), false)
.is_ignore()
{
Some(false)
} else {
None
}
}
_ => decision,
};
let potential_new_ignore: Option<&RepoPath<PathBuf>> = match direction {
ChangesDirection::Revert => None,
ChangesDirection::Record => match changes[i] {
Record::FileAdd { ref name, .. } => Some(&name),
_ => None,
},
};
let (e, f, file_d) = match decision {
Some(true) => ('Y', final_decision, file_decision),
Some(false) => ('N', final_decision, file_decision),
None => {
if let Some(d) = final_decision {
(d, Some(d), file_decision)
} else {
let command_decisions = if let Some(ref f) = current_file {
file_decision.and_then(|d| match changes[i] {
Record::Change { ref file, .. } => {
if f == file {
Some((d, final_decision, Some(d)))
} else {
None
}
}
_ => None,
})
} else {
None
};
if let Some(res) = command_decisions {
res
} else {
prompt_one_change(
repository,
repo_root,
&changes[i],
&mut current_file,
changes.len(),
i,
direction,
potential_new_ignore,
&mut terminal,
&getch,
cwd,
show_help,
)?
}
}
}
};
show_help = false;
final_decision = f;
file_decision = file_d;
match e {
'Y' => {
choices.insert(i, direction.is_record());
match changes[i] {
Record::FileAdd { ref name, .. } => {
to_unadd.remove(&name);
}
_ => (),
}
i += 1
}
'N' => {
choices.insert(i, !direction.is_record());
i += 1
}
'K' if i > 0 => {
choices.remove(&i);
i -= 1
}
'I' => match potential_new_ignore {
Some(file) => {
add_to_ignore_file(
file,
repo_root,
&mut new_ignored_patterns,
&mut new_ignore_builder,
);
choices.insert(i, !direction.is_record());
i += 1;
}
_ => {}
},
'?' => {
show_help = true;
}
_ => {}
}
}
Ok((choices, new_ignored_patterns))
}
fn read_line(s: &str) -> String {
print!("{}", s);
if let Some(mut term) = line::Terminal::new() {
term.read_line().unwrap()
} else {
let stdin = std::io::stdin();
let mut stdin = stdin.lock().lines();
if let Some(Ok(x)) = stdin.next() {
x
} else {
String::new()
}
}
}
pub fn read_line_with_suggestion(prompt: &str, _suggestion: &str) -> String {
read_line(prompt)
}
pub fn ask_authors() -> Result<Vec<String>, Error> {
std::io::stdout().flush()?;
Ok(vec![read_line("What is your name <and email address>? ")])
}
pub fn ask_patch_name(
repo_root: &RepoRoot<impl AsRef<Path>>,
maybe_editor: Option<&String>,
template: String,
) -> Result<(String, Option<String>), Error> {
let repo_root = repo_root.repo_root.as_ref();
if let Some(editor) = maybe_editor {
let mut patch_name_file = repo_root.to_path_buf();
patch_name_file.push(PIJUL_DIR_NAME);
patch_name_file.push("PATCH_NAME");
debug!("patch name file: {:?}", patch_name_file);
let _ =
File::create(patch_name_file.as_path())?.write_all(template.into_bytes().as_slice())?;
let mut editor_cmd = editor
.trim()
.split(" ")
.map(OsString::from)
.collect::<Vec<_>>();
editor_cmd.push(patch_name_file.clone().into_os_string());
process::Command::new(&editor_cmd[0])
.args(&editor_cmd[1..])
.current_dir(repo_root)
.status()
.map_err(|e| Error::CannotSpawnEditor {
editor: editor.to_owned(),
cause: e.to_string(),
})?;
let mut patch_name =
File::open(patch_name_file.as_path()).map_err(|_| Error::EmptyPatchName)?;
let mut patch_name_content = String::new();
patch_name.read_to_string(&mut patch_name_content)?;
remove_file(patch_name_file)?;
let re_with_desc = Regex::new(r"^([^\n]+)\n\s*(.(?s:.)*)$").unwrap();
let re_without_desc = Regex::new(r"^([^\n]+)\s*$").unwrap();
if let Some(capt) = re_without_desc.captures(patch_name_content.as_ref()) {
debug!("patch name without description");
if capt[1].chars().any(|x| x == '\n' || x == '\r') {
return Err(Error::InvalidPatchName);
}
Ok((String::from(&capt[1]), None))
} else if let Some(capt) = re_with_desc.captures(patch_name_content.as_ref()) {
debug!("patch name with description");
let descr: String = capt[2]
.lines()
.filter(|l| !l.starts_with("#"))
.map(|x| format!("{}\n", x))
.collect::<String>()
.trim()
.into();
if descr.is_empty() {
Ok((String::from(&capt[1]), None))
} else {
Ok((String::from(&capt[1]), Some(String::from(descr.trim()))))
}
} else {
debug!("couldn't get a valid patch name");
debug!("the content was:");
debug!("=======================");
debug!("{}", patch_name_content);
debug!("=======================");
Err(Error::EmptyPatchName)
}
} else {
std::io::stdout().flush()?;
let res = read_line("What is the name of this patch? ");
debug!("res = {:?}", res);
if res.trim().is_empty() {
Err(Error::EmptyPatchName)
} else {
Ok((res, None))
}
}
}
pub fn ask_learn_ssh(host: &str, port: u16, fingerprint: &str) -> Result<bool, Error> {
std::io::stdout().flush()?;
print!(
"The authenticity of host {:?}:{} cannot be established.\nThe fingerprint is {:?}.",
host, port, fingerprint
);
let input = read_line("Are you sure you want to continue (yes/no)? ");
let input = input.trim().to_uppercase();
Ok(input == "YES")
}
pub fn print_fileheader(
term: &mut Option<Box<StdoutTerminal>>,
inode_status: ChangeType,
file: &RepoPath<impl AsRef<Path>>,
repo_root: &RepoRoot<impl AsRef<Path>>,
cwd: &Path,
) {
if let Some(ref mut term) = *term {
term.attr(Attr::Bold).unwrap_or(());
term.attr(Attr::Underline(true)).unwrap_or(());
}
match inode_status {
ChangeType::Modified => println!("{}", pretty_repo_path(repo_root, file, cwd).display()),
ChangeType::New => {
if let Some(ref mut term) = *term {
term.fg(term::color::CYAN).unwrap_or(());
};
println!(
"new file {}",
pretty_repo_path(repo_root, file, cwd).display()
)
}
ChangeType::Del => {
if let Some(ref mut term) = *term {
term.fg(term::color::MAGENTA).unwrap_or(());
};
println!(
"deleted file {}",
pretty_repo_path(repo_root, file, cwd).display()
)
}
ChangeType::Move {
ref former_names,
ref new_name,
} => {
let mut s = String::new();
if former_names.len() == 1 {
s.push_str(&format!("{} →", former_names[0].display()));
} else if former_names.len() > 1 {
s.push_str("{ ");
for (k, former_name) in former_names.iter().enumerate() {
if k > 0 {
s.push_str(", ");
}
s.push_str(&former_name.display().to_string())
}
s.push_str(" } →");
} else {
s.push_str("→");
}
if let Some(ref mut term) = *term {
term.fg(term::color::YELLOW).unwrap_or(());
}
println!(
"{} {}",
s,
pretty_repo_path(repo_root, new_name, cwd).display()
);
}
};
if let Some(ref mut term) = *term {
term.reset().unwrap_or(());
}
}
pub fn print_diff<T: rand::Rng>(
repo_root: &RepoRoot<impl AsRef<Path>>,
repository: &MutTxn<T>,
cwd: &Path,
mut file_changes: HashMap<Rc<RepoPath<PathBuf>>, libpijul::status::FileStatus>,
) {
let mut terminal = if atty::is(atty::Stream::Stdout) {
term::stdout()
} else {
None
};
let mut current_file;
for (file, file_status) in file_changes.drain() {
current_file = Some(file.to_owned());
print_fileheader(
&mut terminal,
file_status.inode_status,
&file,
repo_root,
cwd,
);
for r in file_status.records {
print_record(
repo_root,
&mut terminal,
cwd,
repository,
&mut current_file,
&r,
);
println!("")
}
}
}