bundle-sources 0.0.1

library and program for generating source code bundles (eg for AGPL compliance
Documentation
// Copyright 2020 Ian Jackson
// SPDX-License-Identifier: GPL-3.0-or-later
// There is NO WARRANTY.

use crate::imports::*;
use crate::utils::*;
use crate::html::*;

type E = anyhow::Error;

pub trait Component : Debug {
  /// creates <outbasepath>[.<extension>] (eg, <basename>.tar)
  ///  from whatever self refers to
  /// invocation directory is unspecified
  /// returns None or ".<extension>"
  /// if adding .<extension> may also create <basename> temporarily
  fn default_desc(&self) -> Html;
  #[throws(E)]
  fn bundle(&self, outbasepath : &String) -> Option<String>;
}

#[derive(Debug)]
pub struct FileComponent(pub String);
impl Component for FileComponent {
  fn default_desc(&self) -> Html {
    Html(format!(r#"file {}"#, Html::from_literal(&self.0).0))
  }
  #[throws(E)]
  fn bundle(&self, outbasepath : &String) -> Option<String> {
    let src = &self.0;
    fs::copy(src, outbasepath)?;
    None
  }
}

#[derive(Debug)]
pub struct DirectoryComponent(Box<dyn Component>);
impl DirectoryComponent {
  #[throws(E)]
  pub fn new(src : String) -> DirectoryComponent {
    let is_git = match fs::metadata(src.to_owned() + "/.git") {
      Ok(_) => true,
      Err(e) if e.kind() == NotFound => false,
      Err(e) => throw!(e),
    };

    DirectoryComponent(if is_git {
      Box::new( GitComponent(src) )
    } else {
      Box::new( RawDirectoryComponent(src) )
    })
  }
}
impl Component for DirectoryComponent {
  fn default_desc(&self) -> Html { self.0.default_desc() }
  #[throws(E)]
  fn bundle(&self, outbasepath : &String) -> Option<String> {
    self.0.bundle(outbasepath)?
  }
}

#[derive(Debug)]
pub struct RawDirectoryComponent(pub String);
impl Component for RawDirectoryComponent {
  fn default_desc(&self) -> Html {
    Html(format!(r#"directory {}"#, Html::from_literal(&self.0).0))
  }
  #[throws(E)]
  fn bundle(&self, outbasepath : &String) -> Option<String> {
    let src = &self.0;
    let mut split = src.rsplitn(2,'/');
    let srcleaf = split.next().unwrap();
    let dir = split.next().ok_or_else(||
      anyhow!("trying to bundle directory with no parent in pathname"))?;
    let outfile = outbasepath.to_owned() + ".tar";
    let stdout = File::create(&outfile)?;
    let mut cmd = tar_command();
    cmd
      .current_dir(&dir)
      .stdout(stdout)
      .args(&["-cf","-","--",srcleaf]);
    run_cmd(cmd).cxm(||format!("tar for raw directory {:?}", &dir))?;
    Some(".tar".to_owned())
  }
}

#[derive(Debug)]
pub struct GitComponent(pub String);
impl Component for GitComponent {
  fn default_desc(&self) -> Html {
    Html(format!(r#"git working directory {}"#,
                 Html::from_literal(&self.0).0))
  }
  #[throws(E)]
  fn bundle(&self, outbasepath : &String) -> Option<String> {
    let src = &self.0;
    let gitclone = r#"#!/bin/bash
      set -e
      set -o pipefail

      src="$1"; shift
      dest="$1"; shift

      rm -rf "$dest"
      mkdir "$dest"
      cd "$dest"
      desta=$(pwd)

      git init -q

      git fetch -q "$src" HEAD
      git update-ref refs/heads/master FETCH_HEAD

      (
        set -e
        cd "$src"
        git ls-files --exclude-standard -oc -z
      ) | (
        cd "$src"
        cpio -p0 -dum --no-preserve-owner --quiet "$desta"
      )

      git reset -q

      cd ..
    "#;

    let mut c = Command::new("bash");
    c.args(&["-ec",gitclone,"x",&src,outbasepath]);
    run_cmd(c).context("git fetch, copy etc. script")?;

    let r = RawDirectoryComponent(outbasepath.clone()).bundle(outbasepath)?;
    remove_dir_all(outbasepath)?;
    r
  }
}