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


      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<5>, 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<5>) -> Result<(), Error> {
    183     promises.remove(Promise::Unveil);
    184     promises.pledge()
    185 }
    186 /// No-op that returns `Ok`.
    187 #[expect(
    188     clippy::unnecessary_wraps,
    189     reason = "consistent API as openbsd feature"
    190 )]
    191 #[cfg(not(target_os = "openbsd"))]
    192 fn pledge_away_unveil(_: &mut Zst) -> Result<(), Infallible> {
    193     Ok(())
    194 }
    195 /// Removes all `Promise`s except `Stdio`.
    196 #[cfg(target_os = "openbsd")]
    197 fn pledge_away_all_but_stdio(promises: &mut Promises<5>) -> Result<(), Error> {
    198     promises.retain([Promise::Stdio]);
    199     promises.pledge()
    200 }
    201 /// No-op that returns `Ok`.
    202 #[expect(
    203     clippy::unnecessary_wraps,
    204     reason = "consistent API as openbsd feature"
    205 )]
    206 #[cfg(not(target_os = "openbsd"))]
    207 fn pledge_away_all_but_stdio(_: &mut Zst) -> Result<(), Infallible> {
    208     Ok(())
    209 }
    210 /// Calls `unveil_none` on `/`.
    211 #[cfg(target_os = "openbsd")]
    212 fn veil_all() -> Result<(), UnveilErr> {
    213     Permissions::NONE.unveil("/")
    214 }
    215 /// No-op that returns `Ok`.
    216 #[expect(
    217     clippy::unnecessary_wraps,
    218     reason = "consistent API as openbsd feature"
    219 )]
    220 #[cfg(not(target_os = "openbsd"))]
    221 const fn veil_all() -> Result<(), Infallible> {
    222     Ok(())
    223 }
    224 /// Calls `unveil`_on `GIT` with `Permissions::EXECUTE`.
    225 #[cfg(target_os = "openbsd")]
    226 fn unveil_git() -> Result<(), UnveilErr> {
    227     Permissions::EXECUTE.unveil(GIT)
    228 }
    229 /// No-op that returns `Ok`.
    230 #[expect(
    231     clippy::unnecessary_wraps,
    232     reason = "consistent API as openbsd feature"
    233 )]
    234 #[cfg(not(target_os = "openbsd"))]
    235 const fn unveil_git() -> Result<(), Infallible> {
    236     Ok(())
    237 }
    238 /// Calls `unveil`_on `path` with `Permissions::READ`.
    239 #[cfg(target_os = "openbsd")]
    240 fn unveil_read<P: AsRef<Path>>(path: P) -> Result<(), UnveilErr> {
    241     Permissions::READ.unveil(path)
    242 }
    243 /// No-op that returns `Ok`.
    244 #[expect(
    245     clippy::unnecessary_wraps,
    246     reason = "consistent API as openbsd feature"
    247 )]
    248 #[cfg(not(target_os = "openbsd"))]
    249 fn unveil_read<P: AsRef<Path>>(_: P) -> Result<(), Infallible> {
    250     Ok(())
    251 }
    252 /// For each entry in `dir`, `git` is forked and invoked with `-C <dir><entry_in_dir> log -1 --date=iso-strict --pretty=format:"%cd"`.
    253 fn get_git_data(map: &mut BTreeMap<OffsetDateTime, Vec<String>>, dir: AbsDirPath) -> Result<(), E> {
    254     let iso_format = Iso8601::DEFAULT;
    255     for entry in fs::read_dir(dir)? {
    256         let repo = entry?;
    257         if repo.file_type()?.is_dir() {
    258             let path = repo.path().into_os_string().into_string()?;
    259             let output = Command::new(GIT)
    260                 .stderr(Stdio::piped())
    261                 .stdin(Stdio::null())
    262                 .stdout(Stdio::piped())
    263                 .args([
    264                     "-C",
    265                     path.as_str(),
    266                     "log",
    267                     "-1",
    268                     "--date=iso-strict",
    269                     "--pretty=format:%cd",
    270                 ])
    271                 .output()?;
    272             if output.status.success() {
    273                 let value = str::from_utf8(output.stdout.as_slice())?;
    274                 OffsetDateTime::parse(value, &iso_format).map(|time| {
    275                     map.entry(time)
    276                         .or_insert_with(|| Vec::with_capacity(1))
    277                         .push(path);
    278                 })?;
    279             } else {
    280                 return Err(E::GitErr(output.stderr));
    281             }
    282         }
    283     }
    284     Ok(())
    285 }
    286 /// Writes each `String` in each `Vec` to `stdout` with spaces in between in descending order
    287 /// of `map`.
    288 fn write_results(map: BTreeMap<OffsetDateTime, Vec<String>>) -> Result<(), Error> {
    289     let mut stdout = io::stdout().lock();
    290     map.into_values().rev().try_fold((), |(), paths| {
    291         paths
    292             .into_iter()
    293             .try_fold((), |(), path| write!(&mut stdout, "{path} "))
    294     })
    295 }
    296 /// The absolute path to `git`.
    297 const GIT: &str = "/usr/local/bin/git";
    298 fn main() -> Result<(), E> {
    299     let mut promises = pledge_init()?;
    300     veil_all()?;
    301     let git_dir = args::from_env_args()?;
    302     unveil_read(git_dir.as_ref())?;
    303     unveil_git()?;
    304     pledge_away_unveil(&mut promises)?;
    305     let mut map = BTreeMap::new();
    306     get_git_data(&mut map, git_dir)?;
    307     pledge_away_all_but_stdio(&mut promises)?;
    308     write_results(map).map_err(E::Io)
    309 }