use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::bail;
use canonical_path::{CanonicalPath, CanonicalPathBuf};
use chrono::Utc;
use clap::Parser;
use libpijul::change::*;
use libpijul::changestore::*;
use libpijul::{
ArcTxn, Base32, ChannelMutTxnT, ChannelRef, ChannelTxnT, MutTxnTExt, TxnT, TxnTExt,
};
use libpijul::{HashMap, HashSet};
use log::debug;
use crate::repository::*;
#[derive(Parser, Debug)]
pub struct Record {
#[clap(short = 'a', long = "all")]
pub all: bool,
#[clap(short = 'm', long = "message")]
pub message: Option<String>,
#[clap(long = "description")]
pub description: Option<String>,
#[clap(long = "author")]
pub author: Option<String>,
#[clap(long = "channel")]
pub channel: Option<String>,
#[clap(long = "repository")]
pub repo_path: Option<PathBuf>,
#[clap(long = "timestamp", value_parser = parse_datetime_rfc2822)]
pub timestamp: Option<i64>,
#[clap(long = "ignore-missing")]
pub ignore_missing: bool,
#[clap(long = "working-copy")]
pub working_copy: Option<String>,
#[clap(long = "amend")]
#[allow(clippy::option_option)]
pub amend: Option<Option<String>>,
pub prefixes: Vec<PathBuf>,
#[clap(long = "identity")]
pub identity: Option<String>,
#[clap(long = "patience")]
pub patience: bool,
}
pub(crate) fn parse_datetime_rfc2822(s: &str) -> Result<i64, &'static str> {
if let Ok(ts) = chrono::DateTime::parse_from_rfc2822(s) {
return Ok(ts.timestamp());
}
if let Ok(t) = s.parse() {
return Ok(t);
}
Err("Could not parse timestamp")
}
impl Record {
pub async fn run(self) -> Result<(), anyhow::Error> {
let repo = Repository::find_root(self.repo_path.clone())?;
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
for h in repo.config.hooks.record.iter() {
h.run()?
}
let txn = repo.pristine.arc_txn_begin()?;
let cur = txn
.read()
.current_channel()
.unwrap_or(crate::DEFAULT_CHANNEL)
.to_string();
let channel = if let Some(ref c) = self.channel {
c
} else {
cur.as_str()
};
let mut channel = if let Some(channel) = txn.read().load_channel(&channel)? {
channel
} else {
bail!("Channel {:?} not found", channel);
};
let mut extra = Vec::new();
for h in repo.config.extra_dependencies.iter() {
let (h, c) = txn.read().hash_from_prefix(h)?;
if txn
.read()
.get_changeset(txn.read().changes(&*channel.read()), &c)?
.is_none()
{
bail!(
"Change {:?} (from .pijul/config) is not on channel {:?}",
h,
channel.read().name
)
}
extra.push(h)
}
let header = if let Some(ref amend) = self.amend {
let h = if let Some(ref hash) = amend {
txn.read().hash_from_prefix(hash)?.0
} else if let Some(h) = txn.read().reverse_log(&*channel.read(), None)?.next() {
(h?.1).0.into()
} else {
return Ok(());
};
let header = if let Some(message) = self.message.clone() {
ChangeHeader {
message,
..repo.changes.get_header(&h)?
}
} else {
repo.changes.get_header(&h)?
};
txn.write().unrecord(
&repo.changes,
&mut channel,
&h,
self.timestamp.unwrap_or(0) as u64,
)?;
header
} else {
self.header().await?
};
let no_prefixes =
self.prefixes.is_empty() && !self.ignore_missing && self.working_copy.is_none();
let (repo_path, working_copy) = if let Some(ref w) = self.working_copy {
(
CanonicalPathBuf::canonicalize(w)?,
Some(libpijul::working_copy::filesystem::FileSystem::from_root(w)),
)
} else {
(CanonicalPathBuf::canonicalize(&repo.path)?, None)
};
let (key, _password) =
crate::identity::Complete::load(&crate::identity::choose_identity_name(false).await?)?
.decrypt()?;
txn.write()
.apply_root_change_if_needed(&repo.changes, &channel, rand::thread_rng())?;
let result = self.record(
txn,
channel.clone(),
working_copy.as_ref().unwrap_or(&repo.working_copy),
&repo.changes,
repo_path,
header,
&extra,
)?;
match result {
Either::A((txn, mut change, updates, oldest)) => {
let hash = repo.changes.save_change(&mut change, |change, hash| {
change.unhashed = Some(serde_json::json!({
"signature": key.sign_raw(&hash.to_bytes()).unwrap(),
}));
Ok::<_, anyhow::Error>(())
})?;
let mut txn_ = txn.write();
txn_.apply_local_change(&mut channel, &change, &hash, &updates)?;
let mut path = repo.path.join(libpijul::DOT_DIR);
path.push("identities");
std::fs::create_dir_all(&path)?;
writeln!(stdout, "Hash: {}", hash.to_base32())?;
debug!("oldest = {:?}", oldest);
if no_prefixes {
let mut oldest = oldest
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
if oldest == 0 {
oldest = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
}
txn_.touch_channel(&mut *channel.write(), Some((oldest / 1000) * 1000));
}
std::mem::drop(txn_);
txn.commit()?;
}
Either::B(txn) => {
if no_prefixes {
txn.write().touch_channel(&mut *channel.write(), None);
txn.commit()?;
}
writeln!(stderr, "Nothing to record")?;
}
}
Ok(())
}
async fn header(&self) -> Result<ChangeHeader, anyhow::Error> {
let config = crate::config::Global::load();
let mut authors = Vec::new();
let mut b = std::collections::BTreeMap::new();
if let Some(ref a) = self.author {
b.insert("name".to_string(), a.clone());
} else {
let public_key = crate::identity::public_key(
&self
.identity
.clone()
.unwrap_or(crate::identity::choose_identity_name(false).await?),
);
b.insert("key".to_string(), public_key?.key);
}
authors.push(Author(b));
let templates = config
.as_ref()
.ok()
.and_then(|(cfg, _)| cfg.template.as_ref());
let message = if let Some(message) = &self.message {
message.clone()
} else if let Some(message_file) = templates.and_then(|t| t.message.as_ref()) {
match std::fs::read_to_string(message_file) {
Ok(m) => m,
Err(e) => bail!("Could not read message template: {:?}: {}", message_file, e),
}
} else {
String::new()
};
let description = if let Some(description) = &self.description {
Some(description.clone())
} else if let Some(descr_file) = templates.and_then(|t| t.description.as_ref()) {
match std::fs::read_to_string(descr_file) {
Ok(d) => Some(d),
Err(e) => bail!(
"Could not read description template: {:?}: {}",
descr_file,
e
),
}
} else {
None
};
let header = ChangeHeader {
message,
authors,
description,
timestamp: if let Some(t) = self.timestamp {
chrono::DateTime::from_utc(
chrono::NaiveDateTime::from_timestamp_opt(t, 0).unwrap(),
chrono::Utc,
)
} else {
Utc::now()
},
};
Ok(header)
}
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(())
}
fn record<
T: TxnTExt + MutTxnTExt + Sync + Send + 'static,
C: ChangeStore + Send + Clone + 'static,
>(
mut self,
txn: ArcTxn<T>,
channel: ChannelRef<T>,
working_copy: &libpijul::working_copy::FileSystem,
changes: &C,
repo_path: CanonicalPathBuf,
header: ChangeHeader,
extra_deps: &[libpijul::Hash],
) -> Result<
Either<
(
ArcTxn<T>,
Change,
HashMap<usize, libpijul::InodeUpdate>,
std::time::SystemTime,
),
ArcTxn<T>,
>,
anyhow::Error,
> {
let mut state = libpijul::RecordBuilder::new();
if self.ignore_missing {
state.ignore_missing = true;
}
if self.prefixes.is_empty() {
if self.ignore_missing {
for f in ignore::Walk::new(&repo_path) {
let f = f?;
if f.metadata()?.is_file() {
let p = CanonicalPath::new(f.path())?;
let p = p.as_path().strip_prefix(&repo_path).unwrap();
state.record(
txn.clone(),
if self.patience {
libpijul::Algorithm::Patience
} else {
libpijul::Algorithm::default()
},
false,
&libpijul::DEFAULT_SEPARATOR,
channel.clone(),
working_copy,
changes,
p.to_str().unwrap(),
1, )?
}
}
} else {
state.record(
txn.clone(),
if self.patience {
libpijul::Algorithm::Patience
} else {
libpijul::Algorithm::default()
},
false,
&libpijul::DEFAULT_SEPARATOR,
channel.clone(),
working_copy,
changes,
"",
1, )?
}
} else {
self.fill_relative_prefixes()?;
working_copy.record_prefixes(
txn.clone(),
if self.patience {
libpijul::Algorithm::Patience
} else {
libpijul::Algorithm::default()
},
channel.clone(),
changes,
&mut state,
repo_path,
&self.prefixes,
false,
1, self.timestamp.unwrap_or(0) as u64,
)?;
}
let mut rec = state.finish();
if rec.actions.is_empty() {
return Ok(Either::B(txn));
}
if rec.has_binary_files && !self.all {
bail!("Cannot record a binary change interactively. Please use -a.")
}
debug!("TAKING LOCK {}", line!());
let txn_ = txn.write();
let actions = rec
.actions
.into_iter()
.map(|rec| rec.globalize(&*txn_).unwrap())
.collect();
let contents = if let Ok(c) = Arc::try_unwrap(rec.contents) {
c.into_inner()
} else {
unreachable!()
};
let mut change =
LocalChange::make_change(&*txn_, &channel, actions, contents, header, Vec::new())?;
let current: HashSet<_> = change.dependencies.iter().cloned().collect();
for dep in extra_deps.iter() {
if !current.contains(dep) {
change.dependencies.push(*dep)
}
}
debug!("has_binary = {:?}", rec.has_binary_files);
let mut change = if self.all {
change
} else {
let mut o = Vec::new();
debug!("write change");
change.write(changes, None, true, &mut o)?;
debug!("write change done");
let mut with_errors: Option<Vec<u8>> = None;
let change = loop {
let mut bytes = if let Some(ref o) = with_errors {
edit::edit_bytes_with_builder(&o[..], tempfile::Builder::new().suffix(".toml"))?
} else {
edit::edit_bytes_with_builder(&o[..], tempfile::Builder::new().suffix(".toml"))?
};
if bytes.iter().all(|c| (*c as char).is_whitespace()) {
bail!("Empty change")
}
let mut change = std::io::BufReader::new(std::io::Cursor::new(&bytes));
if let Ok(change) =
Change::read_and_deps(&mut change, &mut rec.updatables, &*txn_, &channel)
{
break change;
}
let mut err = SYNTAX_ERROR.as_bytes().to_vec();
err.append(&mut bytes);
with_errors = Some(err)
};
if change.changes.is_empty() {
bail!("Cannot parse change")
}
change
};
let current: HashSet<_> = change.dependencies.iter().cloned().collect();
for dep in extra_deps.iter() {
if !current.contains(dep) {
change.dependencies.push(*dep)
}
}
if change.header.message.trim().is_empty() {
bail!("No change message")
}
debug!("saving change");
std::mem::drop(txn_);
Ok(Either::A((txn, change, rec.updatables, rec.oldest_change)))
}
}
enum Either<A, B> {
A(A),
B(B),
}
const SYNTAX_ERROR: &str = "# Syntax errors, please try again.
# Alternatively, you may delete the entire file (including this
# comment) to abort.
";