| |
}
|
| |
|
| |
/// Get sources from the local node.
|
| - |
pub fn get_sources(repoid: RepoId, commit: Oid, src: &Path) -> Result<(), MessageHelperError> {
|
| + |
pub fn get_sources(
|
| + |
adminlog: &mut AdminLog,
|
| + |
dry_run: bool,
|
| + |
repoid: RepoId,
|
| + |
commit: Oid,
|
| + |
src: &Path,
|
| + |
) -> Result<(), MessageHelperError> {
|
| |
let profile = Profile::load().map_err(MessageHelperError::Profile)?;
|
| |
let storage = profile.storage.path();
|
| |
let repo_path = storage.join(repoid.canonical());
|
| |
|
| - |
git_clone(&repo_path, src)?;
|
| - |
git_checkout(commit, src)?;
|
| + |
git_clone(adminlog, dry_run, &repo_path, src)?;
|
| + |
git_checkout(adminlog, dry_run, commit, src)?;
|
| |
|
| |
Ok(())
|
| |
}
|
| |
|
| |
/// Run `git clone` for the repository.
|
| - |
fn git_clone(repo_path: &Path, src: &Path) -> Result<(), MessageHelperError> {
|
| + |
fn git_clone(
|
| + |
adminlog: &mut AdminLog,
|
| + |
dry_run: bool,
|
| + |
repo_path: &Path,
|
| + |
src: &Path,
|
| + |
) -> Result<(), MessageHelperError> {
|
| |
let repo_path = repo_path.to_string_lossy();
|
| |
let src = src.to_string_lossy();
|
| - |
runcmd(&nonempty!["git", "clone", &repo_path, &src], Path::new("."))?;
|
| + |
runcmd(
|
| + |
adminlog,
|
| + |
dry_run,
|
| + |
&nonempty!["git", "clone", &repo_path, &src],
|
| + |
Path::new("."),
|
| + |
)?;
|
| |
Ok(())
|
| |
}
|
| |
|
| |
// Check out the requested commit.
|
| - |
fn git_checkout(commit: Oid, src: &Path) -> Result<(), MessageHelperError> {
|
| + |
fn git_checkout(
|
| + |
adminlog: &mut AdminLog,
|
| + |
dry_run: bool,
|
| + |
commit: Oid,
|
| + |
src: &Path,
|
| + |
) -> Result<(), MessageHelperError> {
|
| |
runcmd(
|
| + |
adminlog,
|
| + |
dry_run,
|
| |
&nonempty!["git", "config", "advice.detachedHead", "false"],
|
| |
src,
|
| |
)?;
|
| |
let commit = commit.to_string();
|
| - |
runcmd(&nonempty!["git", "checkout", &commit], src)?;
|
| + |
runcmd(
|
| + |
adminlog,
|
| + |
dry_run,
|
| + |
&nonempty!["git", "checkout", &commit],
|
| + |
src,
|
| + |
)?;
|
| |
Ok(())
|
| |
}
|
| |
|
| |
/// Run a program.
|
| - |
pub fn runcmd(argv: &NonEmpty<&str>, cwd: &Path) -> Result<i32, MessageHelperError> {
|
| + |
pub fn runcmd(
|
| + |
adminlog: &mut AdminLog,
|
| + |
dry_run: bool,
|
| + |
argv: &NonEmpty<&str>,
|
| + |
cwd: &Path,
|
| + |
) -> Result<(i32, Vec<u8>), MessageHelperError> {
|
| + |
if dry_run {
|
| + |
adminlog
|
| + |
.writeln(&format!("runcmd: pretend to run: argv={argv:?}"))
|
| + |
.map_err(MessageHelperError::AdminLog)?;
|
| + |
return Ok((0, vec![]));
|
| + |
}
|
| + |
|
| + |
adminlog.writeln(&format!("runcmd: argv={argv:?}"))?;
|
| |
let output = Command::new("bash")
|
| |
.arg("-c")
|
| |
.arg(r#""$@" 2>&1"#)
|
| |
.map_err(|err| MessageHelperError::Command("bash", err))?;
|
| |
|
| |
let exit = output.status.code().unwrap_or(NO_EXIT);
|
| - |
Ok(exit)
|
| + |
|
| + |
if exit != 0 {
|
| + |
adminlog.writeln(&format!("runcmd: exit={exit}"))?;
|
| + |
indented(adminlog, "stdout", &output.stdout);
|
| + |
indented(adminlog, "stderr", &output.stderr);
|
| + |
}
|
| + |
|
| + |
Ok((exit, output.stdout))
|
| + |
}
|
| + |
|
| + |
/// Log a string with every line indented.
|
| + |
pub fn indented(adminlog: &mut AdminLog, msg: &str, bytes: &[u8]) {
|
| + |
if !bytes.is_empty() {
|
| + |
adminlog.writeln(&format!("{msg}:")).ok();
|
| + |
let text = String::from_utf8_lossy(bytes);
|
| + |
for line in text.lines() {
|
| + |
adminlog.writeln(&format!(" {line}")).ok();
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
|
| + |
/// A log for the administrator, whose duty it is to keep the
|
| + |
/// software running.
|
| + |
#[derive(Debug, Default)]
|
| + |
pub struct AdminLog {
|
| + |
filename: Option<PathBuf>,
|
| + |
file: Option<File>,
|
| + |
}
|
| + |
|
| + |
impl AdminLog {
|
| + |
/// Create an admin log that doesn't write to a file, but to
|
| + |
/// stderr.
|
| + |
pub fn null() -> Self {
|
| + |
Self::default()
|
| + |
}
|
| + |
|
| + |
/// Create an admin log that writes to a named file.
|
| + |
pub fn open(filename: &Path) -> Result<Self, LogError> {
|
| + |
let file = OpenOptions::new()
|
| + |
.append(true)
|
| + |
.create(true)
|
| + |
.open(filename)
|
| + |
.map_err(|e| LogError::OpenLogFile(filename.into(), e))?;
|
| + |
Ok(Self {
|
| + |
filename: Some(filename.into()),
|
| + |
file: Some(file),
|
| + |
})
|
| + |
}
|
| + |
|
| + |
/// Write a line to the admin log.
|
| + |
pub fn writeln(&mut self, text: &str) -> Result<(), LogError> {
|
| + |
self.write("[")?;
|
| + |
self.write(&now()?)?;
|
| + |
self.write("] ")?;
|
| + |
self.write(text)?;
|
| + |
self.write("\n")?;
|
| + |
Ok(())
|
| + |
}
|
| + |
|
| + |
fn write(&mut self, msg: &str) -> Result<(), LogError> {
|
| + |
if let Some(file) = &mut self.file {
|
| + |
#[allow(clippy::unwrap_used)] // we know it's OK
|
| + |
file.write_all(msg.as_bytes())
|
| + |
.map_err(|e| LogError::WriteLogFile(self.filename.clone().unwrap(), e))?;
|
| + |
} else {
|
| + |
eprintln!("ambient adapter: {msg}");
|
| + |
}
|
| + |
Ok(())
|
| + |
}
|
| + |
}
|
| + |
|
| + |
fn now() -> Result<String, LogError> {
|
| + |
let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
|
| + |
OffsetDateTime::now_utc()
|
| + |
.format(fmt)
|
| + |
.map_err(LogError::TimeFormat)
|
| + |
}
|
| + |
|
| + |
/// Possible errors from using the admin log.
|
| + |
#[derive(Debug, thiserror::Error)]
|
| + |
pub enum LogError {
|
| + |
/// Can't open named file.
|
| + |
#[error("failed to open log file {0}")]
|
| + |
OpenLogFile(PathBuf, #[source] std::io::Error),
|
| + |
|
| + |
/// Can't write to file.
|
| + |
#[error("failed to write to log file {0}")]
|
| + |
WriteLogFile(PathBuf, #[source] std::io::Error),
|
| + |
|
| + |
/// Can' format time stamp.
|
| + |
#[error("failed to format time stamp")]
|
| + |
TimeFormat(#[source] time::error::Format),
|
| |
}
|
| |
|
| |
/// Possible errors from this module.
|