use crate::repository::*;
use crate::Error;
use chrono::Utc;
use libanu::change::*;
use libanu::changestore::*;
use libanu::pristine::{Base32, ChannelRef, MutTxnT, TxnT};
use libanu::{MutTxnTExt, TxnTExt};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use thrussh_keys::PublicKeyBase64;
#[derive(Clap, Debug)]
pub struct Record {
#[clap(short = 'a', long = "all")]
pub all: bool,
#[clap(short = 'm', long = "message")]
pub message: 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")]
pub timestamp: Option<i64>,
#[clap(short = 'S')]
pub sign: bool,
#[clap(long = "stdin")]
pub stdin: bool,
#[clap(long = "tag")]
pub tag: bool,
#[clap(long = "amend")]
pub amend: Option<String>,
pub prefixes: Vec<PathBuf>,
}
impl Record {
pub async fn run(self) -> Result<(), anyhow::Error> {
let mut repo = Repository::find_root(self.repo_path.clone())?;
let mut txn = repo.pristine.mut_txn_begin();
let mut channel =
txn.open_or_create_channel(repo.config.get_current_channel(self.channel.as_ref()))?;
let sign = self.sign;
let header = if let Some(ref amend) = self.amend {
let (h, _) = txn.hash_from_prefix(amend)?;
let header = repo.changes.get_header(&h)?;
txn.unrecord(&repo.changes, &mut channel, &h)?;
header
} else {
self.header()
};
let result = self.record(
&mut txn,
&mut channel,
&mut repo.working_copy,
&repo.changes,
&repo.path,
header,
)?;
if let Some((mut change, updates, hash)) = result {
let hash = hash.unwrap();
if sign {
let mut key_path = dirs::home_dir().unwrap().join(".ssh");
if let Some((pk, signature)) = sign_hash(&mut key_path, hash).await? {
let sig = toml::Value::try_from(vec![Signature {
public_key: pk,
timestamp: change.header.timestamp,
signature: signature,
}])?;
let mut toml = toml::map::Map::new();
toml.insert("signatures".to_string(), sig);
change.unhashed = Some(toml.into());
let hash2 = repo.changes.save_change(&change).unwrap();
assert_eq!(hash2, hash);
}
}
txn.apply_local_change(&mut channel, &change, hash, &updates)?;
println!("Hash: {}", hash.to_base32());
txn.commit()?;
} else {
eprintln!("Nothing to record");
}
Ok(())
}
fn header(&self) -> ChangeHeader {
let authors = if let Some(ref a) = self.author {
vec![libanu::change::Author {
name: a.clone(),
full_name: None,
email: None,
}]
} else if let Ok(global) = crate::config::Global::load() {
vec![global.author]
} else {
Vec::new()
};
ChangeHeader {
message: self.message.clone().unwrap_or(String::new()),
authors,
description: None,
timestamp: if let Some(t) = self.timestamp {
chrono::DateTime::from_utc(chrono::NaiveDateTime::from_timestamp(t, 0), chrono::Utc)
} else {
Utc::now()
},
}
}
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: TxnT + TxnTExt + MutTxnTExt, C: ChangeStore>(
mut self,
txn: &mut T,
channel: &mut ChannelRef<T>,
working_copy: &mut libanu::working_copy::FileSystem,
changes: &C,
repo_path: &Path,
header: ChangeHeader,
) -> Result<
Option<(
Change3,
HashMap<usize, libanu::InodeUpdate>,
Option<libanu::pristine::Hash>,
)>,
anyhow::Error,
> {
let mut state = libanu::RecordBuilder::new();
if self.prefixes.is_empty() {
txn.record(
&mut state,
libanu::Algorithm::default(),
channel,
working_copy,
changes,
""
)?
} else {
self.fill_relative_prefixes()?;
working_copy.record_prefixes(
txn,
channel,
changes,
&mut state,
repo_path,
&self.prefixes,
)?;
}
let mut rec = state.finish();
if rec.actions.is_empty() {
return Ok(None);
}
let actions = rec
.actions
.into_iter()
.map(|rec| rec.globalize(txn))
.collect();
let change =
LocalChange3::make_change(txn, channel, actions, rec.contents, header, Vec::new());
let file_name = |local: &Local, _| -> String { format!("{}:{}", local.path, local.line) };
debug!("has_binary = {:?}", rec.has_binary_files);
let mut change = if self.all {
change
} else if rec.has_binary_files {
return Err(Error::RecordBinaryChange.into());
} else {
let mut o = Vec::new();
change.write(changes, None, file_name, true, &mut o)?;
let mut change =
std::io::BufReader::new(std::io::Cursor::new(edit::edit_bytes(&o[..])?));
let change = Change3::read(&mut change, &mut rec.updatables)?;
if change.changes.is_empty() {
return Ok(None);
}
change
};
if change.header.message.trim().is_empty() {
return Err(Error::NoChangeMessage.into());
}
let (dependencies, extra_known) = if self.tag {
full_dependencies(txn, channel)
} else {
dependencies(txn, channel, change.changes.iter())
};
change.dependencies = dependencies;
change.extra_known = extra_known;
debug!("saving change");
let hash = changes.save_change(&change).unwrap();
debug!("saved");
Ok(Some((change, rec.updatables, Some(hash))))
}
}
#[derive(Debug, Serialize, Deserialize)]
struct Signature {
public_key: String,
timestamp: chrono::DateTime<chrono::Utc>,
signature: String,
}
async fn sign_hash(
key_path: &mut PathBuf,
hash: libanu::pristine::Hash,
) -> Result<Option<(String, String)>, anyhow::Error> {
let to_sign = hash.to_bytes();
match thrussh_keys::agent::client::AgentClient::connect_env().await {
Ok(agent) => {
let mut agent = Some(agent);
for k in &["id_ed25519.pub", "id_rsa.pub"] {
key_path.push(k);
if let Ok(key) = thrussh_keys::load_public_key(&key_path) {
debug!("key");
if let Some(a) = agent.take() {
debug!("authenticate future");
if let (_, Ok(sig)) = a.sign_request_base64(&key, &to_sign).await {
key_path.pop();
let key = key.public_key_base64();
return Ok(Some((key, sig)));
}
}
}
key_path.pop();
}
}
Err(e) => {
error!("{:?}", e);
}
}
for k in &["id_ed25519", "id_rsa"] {
key_path.push(k);
if let Some(k) = crate::remote::ssh::load_secret_key(&key_path, k) {
key_path.pop();
let pk = k.public_key_base64();
return Ok(Some((pk, k.sign_detached(&to_sign)?.to_base64())));
} else {
key_path.pop();
}
}
Ok(None)
}