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


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