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 (9306B)


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