use std::collections::HashSet;
use std::io::Write;
use std::path::PathBuf;
use clap::Clap;
use lazy_static::lazy_static;
use libpijul::changestore::ChangeStore;
use libpijul::{MutTxnT, MutTxnTExt, TxnT, TxnTExt};
use log::debug;
use regex::Regex;
use crate::repository::Repository;
#[derive(Clap, Debug)]
pub struct Remote {
#[clap(subcommand)]
subcmd: Option<SubRemote>,
#[clap(long = "repository")]
repo_path: Option<PathBuf>,
}
#[derive(Clap, Debug)]
pub enum SubRemote {
#[clap(name = "delete")]
Delete { remote: String },
}
impl Remote {
pub fn run(self) -> Result<(), anyhow::Error> {
let repo = Repository::find_root(self.repo_path)?;
debug!("{:?}", repo.config);
let mut stdout = std::io::stdout();
match self.subcmd {
None => {
let txn = repo.pristine.txn_begin()?;
for r in txn.iter_remotes("") {
writeln!(stdout, " {}", r.name())?;
}
}
Some(SubRemote::Delete { remote }) => {
let mut txn = repo.pristine.mut_txn_begin();
if !txn.drop_named_remote(&remote)? {
writeln!(std::io::stderr(), "Remote not found: {:?}", remote)?
} else {
txn.commit()?;
}
}
}
Ok(())
}
}
#[derive(Clap, Debug)]
pub struct Push {
#[clap(long = "repository")]
repo_path: Option<PathBuf>,
#[clap(long = "channel")]
channel: Option<String>,
#[clap(long = "all", short = 'a', conflicts_with = "changes")]
all: bool,
#[clap(short = 'k')]
no_cert_check: bool,
#[clap(long = "path")]
path: Option<String>,
to: Option<String>,
#[clap(long = "to-channel")]
to_channel: Option<String>,
#[clap(last = true)]
changes: Vec<String>,
}
#[derive(Clap, Debug)]
pub struct Pull {
#[clap(long = "repository")]
repo_path: Option<PathBuf>,
#[clap(long = "channel")]
channel: Option<String>,
#[clap(long = "all", short = 'a', conflicts_with = "changes")]
all: bool,
#[clap(short = 'k')]
no_cert_check: bool,
#[clap(long = "full")]
full: bool, #[clap(long = "path")]
path: Option<String>,
from: Option<String>,
#[clap(long = "from-channel")]
from_channel: Option<String>,
#[clap(last = true)]
changes: Vec<String>, }
lazy_static! {
static ref CHANNEL: Regex = Regex::new(r#"([^:]*)(:(.*))?"#).unwrap();
}
impl Push {
pub async fn run(self) -> Result<(), anyhow::Error> {
let mut stderr = std::io::stderr();
let repo = Repository::find_root(self.repo_path)?;
debug!("{:?}", repo.config);
let channel_name = repo.config.get_current_channel(self.channel.as_ref());
let remote_name = if let Some(ref rem) = self.to {
rem
} else if let Some(ref def) = repo.config.default_remote {
def
} else {
return Err(crate::Error::MissingRemote.into());
};
let mut push_channel = None;
let remote_channel = if let Some(ref c) = self.to_channel {
let c = CHANNEL.captures(c).unwrap();
push_channel = c.get(3).map(|x| x.as_str());
let c = c.get(1).unwrap().as_str();
if c.is_empty() {
channel_name
} else {
c
}
} else {
channel_name
};
debug!("remote_channel = {:?} {:?}", remote_channel, push_channel);
let mut remote = repo
.remote(
Some(&repo.path),
&remote_name,
remote_channel,
self.no_cert_check,
)
.await?;
let mut txn = repo.pristine.mut_txn_begin();
let mut paths = if let Some(p) = self.path {
vec![p.to_string()]
} else {
vec![]
};
let remote_changes = remote.update_changelist(&mut txn, &paths).await?;
let channel = txn.open_or_create_channel(channel_name)?;
let path = if let Some(path) = paths.pop() {
let (p, ambiguous) = txn.follow_oldest_path(&repo.changes, &channel, &path)?;
if ambiguous {
return Err((crate::Error::AmbiguousPath { path: path.clone() }).into());
}
Some(p)
} else {
None
};
let mut to_upload = Vec::new();
for (_, (h, m)) in txn.reverse_log(&channel.borrow(), None) {
if let Some(ref remote_changes) = remote_changes {
if txn.remote_has_state(remote_changes, m) {
break;
}
let h_int = txn.get_internal(h).unwrap();
if !txn.remote_has_change(&remote_changes, h) {
if let Some(ref p) = path {
if txn.get_touched_files(*p, Some(h_int)).is_some() {
to_upload.push(h)
}
} else {
to_upload.push(h)
}
}
} else if let crate::remote::RemoteRepo::LocalChannel(ref remote_channel) = remote {
if let Some(channel) = txn.load_channel(remote_channel) {
let channel = channel.borrow();
let h_int = txn.get_internal(h).unwrap();
if txn.get_changeset(&channel.changes, h_int, None).is_none() {
if let Some(ref p) = path {
if txn.get_touched_files(*p, Some(h_int)).is_some() {
to_upload.push(h)
}
} else {
to_upload.push(h)
}
}
}
}
}
to_upload.reverse();
debug!("to_upload = {:?}", to_upload);
if to_upload.is_empty() {
writeln!(stderr, "Nothing to push")?;
return Ok(());
}
let to_upload = if !self.changes.is_empty() {
let mut u = Vec::new();
let mut not_found = Vec::new();
for change in self.changes.iter() {
match txn.hash_from_prefix(change) {
Ok((hash, _)) => {
if to_upload.contains(&hash) {
u.push(hash);
}
}
Err(_) => {
if !not_found.contains(change) {
not_found.push(change.to_string());
}
}
}
}
if !not_found.is_empty() {
return Err((crate::Error::ChangesNotFound { hashes: not_found }).into());
}
check_deps(&repo.changes, &to_upload, &u)?;
u
} else if self.all {
to_upload
} else {
let mut o = make_changelist(&repo.changes, &to_upload)?;
loop {
let d = parse_changelist(&edit::edit_bytes(&o[..])?);
let comp = complete_deps(&repo.changes, &to_upload, &d)?;
if comp.len() == d.len() {
break comp;
}
o = make_changelist(&repo.changes, &comp)?
}
};
debug!("to_upload = {:?}", to_upload);
if to_upload.is_empty() {
writeln!(stderr, "Nothing to push")?;
return Ok(());
}
remote
.upload_changes(&mut txn, repo.changes_dir.clone(), push_channel, &to_upload)
.await?;
txn.commit()?;
remote.finish().await?;
Ok(())
}
}
impl Pull {
pub async fn run(self) -> Result<(), anyhow::Error> {
let mut repo = Repository::find_root(self.repo_path)?;
let mut txn = repo.pristine.mut_txn_begin();
let channel_name = repo.config.get_current_channel(self.channel.as_ref());
let mut channel = txn.open_or_create_channel(channel_name)?;
debug!("{:?}", repo.config);
let remote_name = if let Some(ref rem) = self.from {
rem
} else if let Some(ref def) = repo.config.default_remote {
def
} else {
return Err(crate::Error::MissingRemote.into());
};
let from_channel = if let Some(ref c) = self.from_channel {
c
} else {
crate::DEFAULT_CHANNEL
};
let mut remote = repo
.remote(
Some(&repo.path),
&remote_name,
from_channel,
self.no_cert_check,
)
.await?;
debug!("downloading");
let to_download = if self.changes.is_empty() {
let mut paths = if let Some(p) = self.path {
vec![p]
} else {
vec![]
};
let remote_changes = remote.update_changelist(&mut txn, &paths).await?;
debug!("changelist done");
let mut to_download = Vec::new();
if let Some(ref remote_changes) = remote_changes {
for (_, (h, m)) in txn.iter_remote(&remote_changes.borrow().remote, 0) {
if txn.channel_has_state(&channel, m) {
break;
} else if txn.get_revchanges(&channel, h).is_none() {
to_download.push(h)
}
}
} else if let crate::remote::RemoteRepo::LocalChannel(ref remote_channel) = remote {
let path = if let Some(path) = paths.pop() {
let (p, ambiguous) = txn.follow_oldest_path(&repo.changes, &channel, &path)?;
if ambiguous {
return Err((crate::Error::AmbiguousPath { path: path.clone() }).into());
}
Some(p)
} else {
None
};
if let Some(remote_channel) = txn.load_channel(remote_channel) {
let remote_channel = remote_channel.borrow();
for (_, (h, m)) in txn.reverse_log(&remote_channel, None) {
if txn.channel_has_state(&channel, m) {
break;
}
let h_int = txn.get_internal(h).unwrap();
if txn
.get_changeset(&channel.borrow().changes, h_int, None)
.is_none()
{
if let Some(ref p) = path {
if txn.get_touched_files(*p, Some(h_int)).is_some() {
to_download.push(h)
}
} else {
to_download.push(h)
}
}
}
}
}
to_download.reverse();
to_download
} else {
let r: Result<Vec<libpijul::Hash>, anyhow::Error> = self
.changes
.iter()
.map(|h| Ok(txn.hash_from_prefix(h)?.0))
.collect();
r?
};
if to_download.is_empty() {
let mut stderr = std::io::stderr();
writeln!(stderr, "Nothing to pull")?;
return Ok(());
}
debug!("recording");
let recorded = txn.record_all(
libpijul::Algorithm::default(),
&mut channel,
&mut repo.working_copy,
&repo.changes,
"",
)?;
let hash = if recorded.actions.is_empty() {
None
} else {
let actions = recorded
.actions
.into_iter()
.map(|rec| rec.globalize(&txn))
.collect();
let mut pending_change = libpijul::change::Change::make_change(
&txn,
&channel,
actions,
recorded.contents,
libpijul::change::ChangeHeader::default(),
Vec::new(),
);
let (dependencies, extra_known) =
libpijul::change::dependencies(&txn, &channel, pending_change.changes.iter());
pending_change.dependencies = dependencies;
pending_change.extra_known = extra_known;
let hash = repo.changes.save_change(&pending_change).unwrap();
txn.apply_local_change(&mut channel, &pending_change, hash, &recorded.updatables)?;
Some(hash)
};
remote
.pull(
&mut repo,
&mut txn,
&mut channel,
to_download.clone(),
self.all,
)
.await?;
if !self.all {
let mut o = make_changelist(&repo.changes, &to_download)?;
let d = loop {
let d = parse_changelist(&edit::edit_bytes(&o[..])?);
let comp = complete_deps(&repo.changes, &to_download, &d)?;
if comp.len() == d.len() {
break comp;
}
o = make_changelist(&repo.changes, &comp)?
};
let mut ws = libpijul::ApplyWorkspace::new();
debug!("to_download = {:?}", to_download);
let progress = indicatif::ProgressBar::new(d.len() as u64);
progress.set_style(
indicatif::ProgressStyle::default_spinner()
.template(" Applying changes {wide_bar} {pos}/{len}"),
);
for h in d.iter() {
txn.apply_change_rec_ws(&repo.changes, &mut channel, *h, &mut ws)?;
progress.inc(1);
}
progress.set_style(
indicatif::ProgressStyle::default_bar()
.template("✓ Applying changes {wide_bar} {pos}/{len}"),
);
progress.finish();
}
debug!("completing changes");
remote
.complete_changes(&repo, &txn, &mut channel, &to_download, self.full)
.await?;
remote.finish().await?;
let progress = indicatif::ProgressBar::new_spinner();
progress.set_style(
indicatif::ProgressStyle::default_spinner().template("{spinner} Outputting repository"),
);
progress.enable_steady_tick(100);
txn.output_repository_no_pending(
&mut repo.working_copy,
&repo.changes,
&mut channel,
"",
true,
)?;
progress.set_style(
indicatif::ProgressStyle::default_spinner().template("✓ Outputting repository"),
);
progress.finish();
if let Some(h) = hash {
txn.unrecord(&repo.changes, &mut channel, &h)?;
repo.changes.del_change(&h)?;
}
txn.commit()?;
Ok(())
}
}
fn make_changelist<S: ChangeStore>(
changes: &S,
pullable: &[libpijul::Hash],
) -> Result<Vec<u8>, anyhow::Error> {
use libpijul::Base32;
let mut v = Vec::new();
writeln!(
v,
"# Please select the changes to pull. The lines that contain just a
# valid hash, and no other character (except possibly a newline), will
# be pulled/pushed.\n"
)
.unwrap();
let mut first_p = true;
for p in pullable {
if !first_p {
writeln!(v, "").unwrap();
}
first_p = false;
writeln!(v, "{}\n", p.to_base32()).unwrap();
let deps = changes.get_dependencies(&p)?;
if !deps.is_empty() {
write!(v, " Dependencies:").unwrap();
for d in deps {
write!(v, " {}", d.to_base32()).unwrap();
}
writeln!(v).unwrap();
}
let change = changes.get_header(&p)?;
write!(v, " Author: [").unwrap();
let mut first = true;
for a in change.authors.iter() {
if !first {
write!(v, ", ").unwrap();
}
first = false;
write!(v, "{}", a).unwrap();
}
writeln!(v, "]").unwrap();
writeln!(v, " Date: {}\n", change.timestamp).unwrap();
for l in change.message.lines() {
writeln!(v, " {}", l).unwrap();
}
if let Some(desc) = change.description {
writeln!(v).unwrap();
for l in desc.lines() {
writeln!(v, " {}", l).unwrap();
}
}
}
Ok(v)
}
fn parse_changelist(o: &[u8]) -> Vec<libpijul::Hash> {
use libpijul::Base32;
if let Ok(o) = std::str::from_utf8(o) {
o.lines()
.filter_map(|l| libpijul::Hash::from_base32(l.as_bytes()))
.collect()
} else {
Vec::new()
}
}
fn complete_deps<C: ChangeStore>(
c: &C,
original: &[libpijul::Hash],
now: &[libpijul::Hash],
) -> Result<Vec<libpijul::Hash>, anyhow::Error> {
let original_: HashSet<_> = original.iter().collect();
let mut now_ = HashSet::new();
let mut result = Vec::new();
for n in now {
for d in c.get_dependencies(n)? {
if original_.get(&d).is_some() && now_.get(&d).is_none() {
result.push(d);
now_.insert(d);
}
}
if now_.insert(*n) {
result.push(*n)
}
}
Ok(result)
}
fn check_deps<C: ChangeStore>(
c: &C,
original: &[libpijul::Hash],
now: &[libpijul::Hash],
) -> Result<(), anyhow::Error> {
let original_: HashSet<_> = original.iter().collect();
let now_: HashSet<_> = now.iter().collect();
for n in now {
for d in c.get_dependencies(n)? {
if original_.get(&d).is_some() && now_.get(&d).is_none() {
return Err((crate::Error::MissingDep { h: *n }).into());
}
}
}
Ok(())
}