use std::collections::{BTreeMap, BTreeSet};
use std::io::Write;
use std::path::PathBuf;
use canonical_path::CanonicalPathBuf;
use clap::{Parser, ValueHint};
use libpijul::change::*;
use libpijul::{MutTxnT, TxnT, TxnTExt};
use serde_derive::Serialize;
use pijul_repository::*;
#[derive(Parser, Debug)]
pub struct Diff {
#[clap(long = "repository", value_hint = ValueHint::DirPath)]
pub repo_path: Option<PathBuf>,
#[clap(long = "json")]
pub json: bool,
#[clap(long = "channel")]
pub channel: Option<String>,
#[clap(long = "tag")]
pub tag: bool,
#[clap(short = 's', long = "short")]
pub short: bool,
#[clap(short = 'u', long = "untracked")]
pub untracked: bool,
pub prefixes: Vec<PathBuf>,
#[clap(long = "patience")]
pub patience: bool,
}
impl Diff {
pub fn run(mut self) -> Result<(), anyhow::Error> {
let repo = Repository::find_root(self.repo_path.clone())?;
let txn = repo.pristine.arc_txn_begin()?;
let mut stdout = std::io::stdout();
if self.untracked && self.json {
serde_json::to_writer_pretty(
&mut std::io::stdout(),
&untracked(&repo, txn.clone())?.collect::<Result<Vec<_>, _>>()?,
)?;
writeln!(stdout)?;
return Ok(());
}
let cur = txn
.read()
.current_channel()
.unwrap_or(libpijul::DEFAULT_CHANNEL)
.to_string();
let channel = if let Some(ref c) = self.channel {
c
} else {
cur.as_str()
};
let channel = txn.write().open_or_create_channel(&channel)?;
let mut state = libpijul::RecordBuilder::new();
if self.prefixes.is_empty() {
state.record(
txn.clone(),
if self.patience {
libpijul::Algorithm::Patience
} else {
libpijul::Algorithm::default()
},
self.short,
&libpijul::DEFAULT_SEPARATOR,
channel.clone(),
&repo.working_copy,
&repo.changes,
"",
std::thread::available_parallelism()?.get(),
)?
} else {
self.fill_relative_prefixes()?;
repo.working_copy.record_prefixes(
txn.clone(),
if self.patience {
libpijul::Algorithm::Patience
} else {
libpijul::Algorithm::default()
},
channel.clone(),
&repo.changes,
&mut state,
CanonicalPathBuf::canonicalize(&repo.path)?,
&self.prefixes,
false,
std::thread::available_parallelism()?.get(),
0,
)?;
}
let rec = state.finish();
if rec.actions.is_empty() {
if self.short && self.untracked {
for path in untracked(&repo, txn.clone())? {
writeln!(stdout, "U {}", path?.to_str().unwrap())?;
}
} else if self.untracked {
for path in untracked(&repo, txn.clone())? {
writeln!(stdout, "{}", path?.to_str().unwrap())?;
}
}
return Ok(());
}
let actions: Vec<_> = {
let txn_ = txn.read();
rec.actions
.into_iter()
.map(|rec| rec.globalize(&*txn_).unwrap())
.collect()
};
let actions_is_empty = actions.is_empty();
let contents = if let Ok(cont) = std::sync::Arc::try_unwrap(rec.contents) {
cont.into_inner()
} else {
unreachable!()
};
let mut change = LocalChange::make_change(
&*txn.read(),
&channel,
actions,
contents,
ChangeHeader::default(),
Vec::new(),
)?;
let (dependencies, extra_known) = {
let txn_ = txn.read();
if self.tag {
full_dependencies(&*txn_, &channel)?
} else {
dependencies(&*txn_, &*channel.read(), change.changes.iter())?
}
};
change.dependencies = dependencies;
change.extra_known = extra_known;
let colors = is_colored(repo.config.pager.as_ref());
if self.json {
let mut changes = BTreeMap::new();
for ch in change.changes.iter() {
changes
.entry(ch.path())
.or_insert_with(Vec::new)
.push(Status {
operation: match ch {
Hunk::FileMove { .. } => "file move",
Hunk::FileDel { .. } => "file del",
Hunk::FileUndel { .. } => "file undel",
Hunk::SolveNameConflict { .. } => "solve name conflict",
Hunk::UnsolveNameConflict { .. } => "unsolve name conflict",
Hunk::FileAdd { .. } => "file add",
Hunk::Edit { .. } => "edit",
Hunk::Replacement { .. } => "replacement",
Hunk::SolveOrderConflict { .. } => "solve order conflict",
Hunk::UnsolveOrderConflict { .. } => "unsolve order conflict",
Hunk::ResurrectZombies { .. } => "resurrect zombies",
Hunk::AddRoot { .. } => "root",
Hunk::DelRoot { .. } => "unroot",
},
line: ch.line(),
});
}
serde_json::to_writer_pretty(&mut std::io::stdout(), &changes)?;
writeln!(stdout)?;
} else if self.short {
let mut changes = BTreeMap::new();
for ch in change.changes.iter() {
match ch {
Hunk::FileMove { path, .. } => {
changes.entry(path).or_insert(BTreeSet::new()).insert("MV")
}
Hunk::FileDel { path, .. } => {
changes.entry(path).or_insert(BTreeSet::new()).insert("D")
}
Hunk::FileUndel { path, .. } => {
changes.entry(path).or_insert(BTreeSet::new()).insert("UD")
}
Hunk::FileAdd { path, .. } => {
changes.entry(path).or_insert(BTreeSet::new()).insert("A")
}
Hunk::SolveNameConflict { path, .. } => {
changes.entry(path).or_insert(BTreeSet::new()).insert("SC")
}
Hunk::UnsolveNameConflict { path, .. } => {
changes.entry(path).or_insert(BTreeSet::new()).insert("UC")
}
Hunk::Edit {
local: Local { path, .. },
..
} => changes.entry(path).or_insert(BTreeSet::new()).insert("M"),
Hunk::Replacement {
local: Local { path, .. },
..
} => changes.entry(path).or_insert(BTreeSet::new()).insert("R"),
Hunk::SolveOrderConflict {
local: Local { path, .. },
..
} => changes.entry(path).or_insert(BTreeSet::new()).insert("SC"),
Hunk::UnsolveOrderConflict {
local: Local { path, .. },
..
} => changes.entry(path).or_insert(BTreeSet::new()).insert("UC"),
Hunk::ResurrectZombies {
local: Local { path, .. },
..
} => changes.entry(path).or_insert(BTreeSet::new()).insert("RZ"),
Hunk::AddRoot { .. } | Hunk::DelRoot { .. } => true,
};
}
let al = changes
.iter()
.map(|(_, v)| v.iter().map(|x| x.len()).sum::<usize>() + v.len() - 1)
.max()
.unwrap_or(0);
let spaces: String = std::iter::repeat(' ').take(al).collect();
for (k, v) in changes.iter() {
let mut is_first = true;
for v in v.iter() {
if is_first {
write!(stdout, "{}", v)?;
} else {
write!(stdout, ",{}", v)?;
}
is_first = false;
}
let (sp, _) = spaces.split_at(al - v.len());
writeln!(stdout, "{} {}", sp, k)?;
}
if self.untracked {
for path in untracked(&repo, txn.clone())? {
writeln!(stdout, "U {}", path?.to_str().unwrap())?;
}
}
} else if self.untracked {
for path in untracked(&repo, txn.clone())? {
writeln!(stdout, "{}", path?.to_str().unwrap())?;
}
} else {
match change.write(
&repo.changes,
None,
true,
Colored {
w: termcolor::StandardStream::stdout(termcolor::ColorChoice::Auto),
colors,
},
) {
Ok(()) => {}
Err(libpijul::change::TextSerError::Io(e))
if e.kind() == std::io::ErrorKind::BrokenPipe => {}
Err(e) => return Err(e.into()),
}
}
if actions_is_empty && self.prefixes.is_empty() {
use libpijul::ChannelMutTxnT;
{
let mut txn_ = txn.write();
txn_.touch_channel(&mut *channel.write(), None);
}
txn.commit()?;
}
Ok(())
}
fn fill_relative_prefixes(&mut self) -> Result<(), anyhow::Error> {
let cwd = std::env::current_dir()?;
for p in self.prefixes.iter_mut() {
if p.is_relative() {
*p = cwd.join(&p);
}
}
Ok(())
}
}
#[derive(Debug, Serialize)]
struct Status {
operation: &'static str,
line: Option<usize>,
}
pub struct Colored<W> {
pub w: W,
pub colors: bool,
}
impl<W: std::io::Write> std::io::Write for Colored<W> {
fn write(&mut self, s: &[u8]) -> Result<usize, std::io::Error> {
self.w.write(s)
}
fn flush(&mut self) -> Result<(), std::io::Error> {
self.w.flush()
}
}
use termcolor::*;
impl<W: termcolor::WriteColor> libpijul::change::WriteChangeLine for Colored<W> {
fn write_change_line(&mut self, pref: &str, contents: &str) -> Result<(), std::io::Error> {
if self.colors {
let col = if pref == "+" {
Color::Green
} else {
Color::Red
};
self.w.set_color(ColorSpec::new().set_fg(Some(col)))?;
writeln!(self.w, "{} {}", pref, contents)?;
self.w.reset()
} else {
writeln!(self.w, "{} {}", pref, contents)
}
}
fn write_change_line_binary(
&mut self,
pref: &str,
contents: &[u8],
) -> Result<(), std::io::Error> {
if self.colors {
let col = if pref == "+" {
Color::Green
} else {
Color::Red
};
self.w.set_color(ColorSpec::new().set_fg(Some(col)))?;
write!(
self.w,
"{}b{}",
pref,
data_encoding::BASE64.encode(contents)
)?;
self.w.reset()
} else {
write!(
self.w,
"{}b{}",
pref,
data_encoding::BASE64.encode(contents)
)
}
}
}
pub fn is_colored(repo_config_pager: Option<&pijul_config::Choice>) -> bool {
let mut colors = atty::is(atty::Stream::Stdout);
if let Ok((global, _)) = pijul_config::Global::load() {
match global.colors {
Some(pijul_config::Choice::Always) => colors = true,
Some(pijul_config::Choice::Never) => colors = false,
_ => {}
}
match global.pager {
Some(pijul_config::Choice::Never) => colors = false,
_ => {
super::pager(repo_config_pager);
}
}
} else {
colors &= super::pager(repo_config_pager);
}
colors
}
fn untracked<T: TxnTExt + Send + Sync + 'static>(
repo: &Repository,
txn: libpijul::ArcTxn<T>,
) -> Result<impl Iterator<Item = Result<PathBuf, std::io::Error>>, anyhow::Error> {
let repo_path = CanonicalPathBuf::canonicalize(&repo.path)?;
let threads = std::thread::available_parallelism()?.get();
let txn_ = txn.clone();
Ok(repo
.working_copy
.iterate_prefix_rec(
repo_path.clone(),
repo_path.clone(),
false,
threads,
move |path, _| {
use path_slash::PathExt;
let path_str = path.to_slash_lossy();
log::debug!("untracked {:?}", path_str);
path_str.is_empty() || txn.read().is_tracked(&path_str).unwrap()
},
)?
.filter_map(move |path| match path {
Err(e) => Some(Err(e)),
Ok((path, _)) => {
use path_slash::PathExt;
let path_str = path.to_slash_lossy();
log::debug!("untracked {:?}", path_str);
if !txn_.read().is_tracked(&path_str).unwrap() {
Some(Ok(path))
} else {
None
}
}
}))
}