git_index

Generates arguments to pass to stagit-index in descending order by commit date.
git clone https://git.philomathiclife.com/repos/git_index
Log | Files | Refs | README

main.rs (8546B)


      1 //! # `git_index`
      2 //!
      3 //! Consult [`README.md`](https://git.philomathiclife.com/git_index/file/README.md.html).
      4 extern crate alloc;
      5 /// Module that parsed passed options into the application.
      6 mod args;
      7 use alloc::collections::BTreeMap;
      8 use args::{AbsDirPath, ArgsErr};
      9 #[cfg(not(target_os = "openbsd"))]
     10 use core::convert::Infallible;
     11 use core::{
     12     error,
     13     fmt::{self, Display, Formatter},
     14     str::{self, Utf8Error},
     15 };
     16 use jiff::{Error as TimeErr, Timestamp, fmt::temporal::DateTimeParser};
     17 #[cfg(not(target_os = "openbsd"))]
     18 use priv_sep as _;
     19 #[cfg(target_os = "openbsd")]
     20 use priv_sep::{NulOrIoErr, Permissions, Promise, Promises};
     21 use std::{
     22     ffi::OsString,
     23     fs,
     24     io::{self, Error, Write as _},
     25     path::Path,
     26     process::{Command, Stdio},
     27 };
     28 /// `()` triggers lints when captured by `let`.
     29 /// This only relevant for the no-op functions.
     30 #[cfg(not(target_os = "openbsd"))]
     31 #[derive(Clone, Copy)]
     32 struct Zst;
     33 /// Module for reading the options passed to the application.
     34 /// Error returned from the program.
     35 enum E {
     36     /// Variant for errors due to incorrect arguments being passed.
     37     Args(ArgsErr),
     38     #[cfg(target_os = "openbsd")]
     39     /// Variant for errors due to calls to `unveil`.
     40     Unveil(NulOrIoErr),
     41     /// Variant for IO errors.
     42     Io(Error),
     43     /// Variant when a git repo directory is not valid UTF-8.
     44     NonUtf8Path(OsString),
     45     /// Variant when git errors.
     46     GitErr(Vec<u8>),
     47     /// Variant when git output is not valid UTF-8.
     48     Git(Utf8Error),
     49     /// Variant when git output is not a valid ISO 8601 timestamp.
     50     Time(TimeErr),
     51 }
     52 impl fmt::Debug for E {
     53     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     54         <Self as Display>::fmt(self, f)
     55     }
     56 }
     57 impl Display for E {
     58     #[expect(
     59         clippy::use_debug,
     60         reason = "some contained errors don't implement Display"
     61     )]
     62     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     63         match *self {
     64             Self::Args(ref e) => e.fmt(f),
     65             #[cfg(target_os = "openbsd")]
     66             Self::Unveil(ref err) => write!(f, "unveil(2) failed with {err}"),
     67             Self::Io(ref e) => e.fmt(f),
     68             Self::NonUtf8Path(ref e) => write!(f, "{e:?} is not valid UTF-8"),
     69             Self::GitErr(ref e) => match str::from_utf8(e.as_slice()) {
     70                 Ok(err) => write!(f, "git errored with: {err}"),
     71                 Err(err) => write!(f, "git errored with an invalid UTF-8 error: {err}"),
     72             },
     73             Self::Git(ref e) => write!(
     74                 f,
     75                 "git succeeded, but its output was not valid UTF-8: {e:?}"
     76             ),
     77             Self::Time(ref e) => write!(
     78                 f,
     79                 "git succeeded, but its output is not a valid ISO 8601 timestamp: {e}"
     80             ),
     81         }
     82     }
     83 }
     84 impl error::Error for E {}
     85 impl From<ArgsErr> for E {
     86     fn from(value: ArgsErr) -> Self {
     87         Self::Args(value)
     88     }
     89 }
     90 impl From<Error> for E {
     91     fn from(value: Error) -> Self {
     92         Self::Io(value)
     93     }
     94 }
     95 #[cfg(target_os = "openbsd")]
     96 impl From<NulOrIoErr> for E {
     97     fn from(value: NulOrIoErr) -> Self {
     98         Self::Unveil(value)
     99     }
    100 }
    101 #[cfg(not(target_os = "openbsd"))]
    102 impl From<Infallible> for E {
    103     fn from(value: Infallible) -> Self {
    104         match value {}
    105     }
    106 }
    107 impl From<Utf8Error> for E {
    108     fn from(value: Utf8Error) -> Self {
    109         Self::Git(value)
    110     }
    111 }
    112 impl From<OsString> for E {
    113     fn from(value: OsString) -> Self {
    114         Self::NonUtf8Path(value)
    115     }
    116 }
    117 impl From<TimeErr> for E {
    118     fn from(value: TimeErr) -> Self {
    119         Self::Time(value)
    120     }
    121 }
    122 /// Calls `pledge` with only the sys calls necessary for a minimal application
    123 /// to run. Specifically, the `Promise`s `Exec`, `Proc`, `Rpath`, `Stdio`, and `Unveil`
    124 /// are passed.
    125 #[cfg(target_os = "openbsd")]
    126 fn pledge_init() -> Result<Promises, Error> {
    127     let promises = Promises::new([
    128         Promise::Exec,
    129         Promise::Proc,
    130         Promise::Rpath,
    131         Promise::Stdio,
    132         Promise::Unveil,
    133     ]);
    134     match promises.pledge() {
    135         Ok(()) => Ok(promises),
    136         Err(e) => Err(e),
    137     }
    138 }
    139 /// No-op that returns `Ok`.
    140 #[expect(
    141     clippy::unnecessary_wraps,
    142     reason = "consistent API as openbsd feature"
    143 )]
    144 #[cfg(not(target_os = "openbsd"))]
    145 const fn pledge_init() -> Result<Zst, Infallible> {
    146     Ok(Zst)
    147 }
    148 /// Removes `Promise::Unveil`.
    149 #[cfg(target_os = "openbsd")]
    150 fn pledge_away_unveil(promises: &mut Promises) -> Result<(), Error> {
    151     promises.remove_then_pledge(Promise::Unveil)
    152 }
    153 /// No-op that returns `Ok`.
    154 #[expect(
    155     clippy::unnecessary_wraps,
    156     reason = "consistent API as openbsd feature"
    157 )]
    158 #[cfg(not(target_os = "openbsd"))]
    159 const fn pledge_away_unveil(_: &mut Zst) -> Result<(), Infallible> {
    160     Ok(())
    161 }
    162 /// Removes all `Promise`s except `Stdio`.
    163 #[cfg(target_os = "openbsd")]
    164 fn pledge_away_all_but_stdio(promises: &mut Promises) -> Result<(), Error> {
    165     promises.retain_then_pledge([Promise::Stdio])
    166 }
    167 /// No-op that returns `Ok`.
    168 #[expect(
    169     clippy::unnecessary_wraps,
    170     reason = "consistent API as openbsd feature"
    171 )]
    172 #[cfg(not(target_os = "openbsd"))]
    173 const fn pledge_away_all_but_stdio(_: &mut Zst) -> Result<(), Infallible> {
    174     Ok(())
    175 }
    176 /// Calls `unveil_none` on `/`.
    177 #[cfg(target_os = "openbsd")]
    178 fn veil_all() -> Result<(), NulOrIoErr> {
    179     Permissions::NONE.unveil("/")
    180 }
    181 /// No-op that returns `Ok`.
    182 #[expect(
    183     clippy::unnecessary_wraps,
    184     reason = "consistent API as openbsd feature"
    185 )]
    186 #[cfg(not(target_os = "openbsd"))]
    187 const fn veil_all() -> Result<(), Infallible> {
    188     Ok(())
    189 }
    190 /// Calls `unveil`_on `GIT` with `Permissions::EXECUTE`.
    191 #[cfg(target_os = "openbsd")]
    192 fn unveil_git() -> Result<(), NulOrIoErr> {
    193     Permissions::EXECUTE.unveil(GIT)
    194 }
    195 /// No-op that returns `Ok`.
    196 #[expect(
    197     clippy::unnecessary_wraps,
    198     reason = "consistent API as openbsd feature"
    199 )]
    200 #[cfg(not(target_os = "openbsd"))]
    201 const fn unveil_git() -> Result<(), Infallible> {
    202     Ok(())
    203 }
    204 /// Calls `unveil`_on `path` with `Permissions::READ`.
    205 #[cfg(target_os = "openbsd")]
    206 fn unveil_read<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> {
    207     Permissions::READ.unveil(path)
    208 }
    209 /// No-op that returns `Ok`.
    210 #[expect(
    211     clippy::unnecessary_wraps,
    212     reason = "consistent API as openbsd feature"
    213 )]
    214 #[cfg(not(target_os = "openbsd"))]
    215 fn unveil_read<P: AsRef<Path>>(_: P) -> Result<(), Infallible> {
    216     Ok(())
    217 }
    218 /// For each entry in `dir`, `git` is forked and invoked with `-C <dir><entry_in_dir> log -1 --date=iso-strict --pretty=format:"%cd"`.
    219 fn get_git_data(map: &mut BTreeMap<Timestamp, Vec<String>>, dir: AbsDirPath) -> Result<(), E> {
    220     let parser = DateTimeParser::new();
    221     for entry in fs::read_dir(dir)? {
    222         let repo = entry?;
    223         if repo.file_type()?.is_dir() {
    224             let path = repo.path().into_os_string().into_string()?;
    225             let output = Command::new(GIT)
    226                 .stderr(Stdio::piped())
    227                 .stdin(Stdio::null())
    228                 .stdout(Stdio::piped())
    229                 .args([
    230                     "-C",
    231                     path.as_str(),
    232                     "log",
    233                     "-1",
    234                     "--date=iso-strict",
    235                     "--pretty=format:%cd",
    236                 ])
    237                 .output()?;
    238             if output.status.success() {
    239                 let value = str::from_utf8(output.stdout.as_slice())?;
    240                 parser.parse_timestamp(value).map(|time| {
    241                     map.entry(time)
    242                         .or_insert_with(|| Vec::with_capacity(1))
    243                         .push(path);
    244                 })?;
    245             } else {
    246                 return Err(E::GitErr(output.stderr));
    247             }
    248         }
    249     }
    250     Ok(())
    251 }
    252 /// Writes each `String` in each `Vec` to `stdout` with spaces in between in descending order
    253 /// of `map`.
    254 fn write_results(map: BTreeMap<Timestamp, Vec<String>>) -> Result<(), Error> {
    255     let mut stdout = io::stdout().lock();
    256     map.into_values().rev().try_fold((), |(), paths| {
    257         paths
    258             .into_iter()
    259             .try_fold((), |(), path| write!(&mut stdout, "{path} "))
    260     })
    261 }
    262 /// The absolute path to `git`.
    263 const GIT: &str = "/usr/local/bin/git";
    264 fn main() -> Result<(), E> {
    265     let mut promises = pledge_init()?;
    266     veil_all()?;
    267     let git_dir = args::from_env_args()?;
    268     unveil_read(git_dir.as_ref())?;
    269     unveil_git()?;
    270     pledge_away_unveil(&mut promises)?;
    271     let mut map = BTreeMap::new();
    272     get_git_data(&mut map, git_dir)?;
    273     pledge_away_all_but_stdio(&mut promises)?;
    274     write_results(map).map_err(E::Io)
    275 }