use crate::repository::Repository;
use libanu::changestore::ChangeStore;
use libanu::pristine::{MutTxnT, TxnT};
use libanu::{MutTxnTExt, TxnTExt};
use regex::Regex;
use std::collections::HashSet;
use std::path::PathBuf;
#[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 },
#[clap(name = "list")]
List,
}
impl Remote {
pub fn run(self) -> Result<(), anyhow::Error> {
let repo = Repository::find_root(self.repo_path)?;
debug!("{:?}", repo.config);
match self.subcmd {
None | Some(SubRemote::List) => {
let txn = repo.pristine.txn_begin()?;
for r in txn.iter_remotes("") {
println!(" {}", r.name());
}
}
Some(SubRemote::Delete { remote }) => {
let mut txn = repo.pristine.mut_txn_begin();
if !txn.drop_named_remote(&remote)? {
eprintln!("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(short = 'a')]
all: bool,
#[clap(short = 'k', about="Do not check certificates")]
no_cert_check: bool,
#[clap(last = true)]
changes: Vec<String>,
#[clap(long = "path")]
path: Option<String>,
to: Option<String>,
#[clap(long = "to-channel")]
to_channel: Option<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')]
all: bool,
#[clap(short = 'k', about="Do not check certificates")]
no_cert_check: bool,
#[clap(long = "full", about="Download full changes, even when not necessary")]
full: bool, // This can't be symmetric with push
#[clap(last = true, about = "Pull changes from the local repository, not necessarily from a channel")]
changes: Vec<String>, // For local changes only, can't be symmetric.
#[clap(long = "path")]
path: Option<String>,
from: Option<String>,
#[clap(long = "from-channel")]
from_channel: Option<String>,
}
lazy_static! {
static ref CHANNEL: Regex = Regex::new(r#"([^:]*)(:(.*))?"#).unwrap();
}
impl Push {
pub async fn run(self) -> Result<(), anyhow::Error> {
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(&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 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)
}
}
}
if to_upload.is_empty() {
return Ok(());
}
to_upload.reverse();
debug!("to_upload = {:?}", to_upload);
let to_upload = if !self.all {
let o = make_changelist(&repo.changes, &to_upload)?;
let u = parse_changelist(&edit::edit_bytes(&o[..])?);
check_deps(&repo.changes, &to_upload, &u)?;
u
} else {
to_upload
};
remote
.upload_changes(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(&remote_name, from_channel, self.no_cert_check).await?;
debug!("downloading");
let to_download = if self.changes.is_empty() {
let paths = if let Some(p) = self.path {
vec![p.to_string()]
} else {
vec![]
};
let remote_changes = remote.update_changelist(&mut txn, &paths).await?;
debug!("changelist done");
let mut to_download = Vec::new();
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)
}
}
to_download.reverse();
to_download
} else {
let r: Result<Vec<libanu::pristine::Hash>, anyhow::Error> = self
.changes
.iter()
.map(|h| Ok(txn.hash_from_prefix(h)?.0))
.collect();
r?
};
if to_download.is_empty() {
return Ok(());
}
debug!("recording");
let recorded = txn.record_all(
libanu::Algorithm::default(),
&mut channel,
&mut repo.working_copy,
&repo.changes,
"",
)?;
let hash = if recorded.actions.is_empty() {
None
} else {
Some(txn.apply_recorded(&mut channel, recorded, &repo.changes)?)
};
remote
.pull(
&mut repo,
&mut txn,
&mut channel,
to_download.clone(),
self.all,
)
.await?;
if !self.all {
let o = make_changelist(&repo.changes, &to_download)?;
let d = parse_changelist(&edit::edit_bytes(&o[..])?);
check_deps(&repo.changes, &to_download, &d)?;
let mut ws = libanu::ApplyWorkspace::new();
debug!("to_download = {:?}", to_download);
for h in d.iter() {
txn.apply_change_rec_ws(&repo.changes, &mut channel, *h, &mut ws)?;
}
}
debug!("completing changes");
remote
.complete_changes(&repo, &txn, &mut channel, &to_download, self.full)
.await?;
remote.finish().await?;
txn.output_repository_no_pending(
&mut repo.working_copy,
&repo.changes,
&mut channel,
"",
true,
)?;
if let Some(h) = hash {
txn.unrecord(&repo.changes, &mut channel, &h)?;
repo.changes.del_change(&h)?;
}
txn.commit()?;
Ok(())
}
}
/// Make the "changelist", i.e. the list of patches, editable in a
/// text editor.
fn make_changelist<S: ChangeStore>(
changes: &S,
pullable: &[libanu::pristine::Hash],
) -> Result<Vec<u8>, anyhow::Error> {
use libanu::pristine::Base32;
use std::io::Write;
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.\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 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();
}
}
Ok(v)
}
fn parse_changelist(o: &[u8]) -> Vec<libanu::pristine::Hash> {
use libanu::pristine::Base32;
if let Ok(o) = std::str::from_utf8(o) {
o.lines()
.filter_map(|l| libanu::pristine::Hash::from_base32(l.as_bytes()))
.collect()
} else {
Vec::new()
}
}
fn check_deps<C: ChangeStore>(
c: &C,
original: &[libanu::pristine::Hash],
now: &[libanu::pristine::Hash],
) -> Result<(), anyhow::Error> {
let original_: HashSet<_> = original.iter().collect();
let now_: HashSet<_> = now.iter().collect();
for n in now {
// check that all of `now`'s deps are in now or not in original
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(())
}