├── rustfmt.toml ├── .gitignore ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── Cargo.toml ├── completions ├── pkghist.fish ├── _pkghist └── pkghist.bash ├── LICENSE ├── src ├── main.rs ├── pacman │ ├── group.rs │ ├── action.rs │ ├── newest.rs │ ├── range.rs │ ├── mod.rs │ └── filter.rs ├── error.rs ├── opt │ ├── cli.rs │ └── mod.rs └── pkghist │ ├── mod.rs │ └── format.rs ├── README.md └── Cargo.lock /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | .idea 9 | *.iml -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | name: build 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Install stable toolchain 15 | uses: actions-rs/toolchain@v1 16 | with: 17 | toolchain: stable 18 | override: true 19 | 20 | - name: Run cargo-tarpaulin 21 | uses: actions-rs/tarpaulin@v0.1 22 | with: 23 | version: 0.16.0 24 | 25 | 26 | - name: Upload to codecov.io 27 | uses: codecov/codecov-action@v1.0.2 28 | with: 29 | token: ${{secrets.CODECOV_TOKEN}} 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pkghist" 3 | description = "Query the local version history of packages" 4 | version = "0.7.0" 5 | authors = ["Dennis Mellert "] 6 | edition = "2018" 7 | repository = "https://github.com/herzrasen/pkghist" 8 | readme = "README.md" 9 | keywords = ["pacman", "arch", "pkg", "arch-linux"] 10 | license-file = "LICENSE" 11 | build = "build.rs" 12 | 13 | [dependencies] 14 | clap = { version = "*", features = ["cargo"]} 15 | termion = "*" 16 | chrono = "*" 17 | regex = "1.5.5" 18 | serde = { version = "1", features = ["derive"] } 19 | serde_json = "1" 20 | lazy_static = "1" 21 | itertools = "0" 22 | uuid = { version = "0", features = ["v4"]} 23 | filepath = "0" 24 | 25 | [build-dependencies] 26 | clap = "*" 27 | regex = "1" 28 | chrono = "*" 29 | clap_complete = "4.2.3" 30 | -------------------------------------------------------------------------------- /completions/pkghist.fish: -------------------------------------------------------------------------------- 1 | complete -c pkghist -s o -l output-format -d 'Select the output format' -r -f -a "{json ,plain ,compact }" 2 | complete -c pkghist -s l -l logfile -d 'Specify a logfile' -r 3 | complete -c pkghist -s L -l limit -d 'How many versions to go back in report. [limit > 0]' -r 4 | complete -c pkghist -l first -d 'Output the first \'n\' pacman events' -r 5 | complete -c pkghist -l last -d 'Output the last \'n\' pacman events' -r 6 | complete -c pkghist -s a -l after -d 'Only consider events that occurred after \'date\' [Format: "YYYY-MM-DD HH:MM"]' -r 7 | complete -c pkghist -s r -l with-removed -d 'Include packages that are currently uninstalled' 8 | complete -c pkghist -s R -l removed-only -d 'Only output packages that are currently uninstalled' 9 | complete -c pkghist -l no-colors -d 'Disable colored output' 10 | complete -c pkghist -l no-details -d 'Only output the package names' 11 | complete -c pkghist -s x -l exclude -d 'If set, every filter result will be excluded.' 12 | complete -c pkghist -s h -l help -d 'Print help' 13 | complete -c pkghist -s V -l version -d 'Print version' 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dennis Mellert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use std::env; 3 | 4 | pub mod error; 5 | pub mod opt; 6 | pub mod pacman; 7 | pub mod pkghist; 8 | 9 | fn main() -> Result<(), Error> { 10 | let args: Vec = env::args().collect(); 11 | run(args) 12 | } 13 | 14 | fn run(args: Vec) -> Result<(), Error> { 15 | let matches = opt::parse_args(&args); 16 | let config = opt::Config::from_arg_matches(&matches); 17 | pkghist::run(config) 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use std::fs; 23 | use std::fs::File; 24 | use std::io::Write; 25 | 26 | use super::*; 27 | use filepath::FilePath; 28 | 29 | #[test] 30 | fn should_run() { 31 | let file_name = uuid::Uuid::new_v4().to_string(); 32 | let mut file = File::create(&file_name).unwrap(); 33 | writeln!( 34 | file, 35 | "[2019-07-14 21:33] [PACMAN] synchronizing package lists\n\ 36 | [2019-07-14 21:33] [PACMAN] starting full system upgrade\n\ 37 | [2019-07-14 21:33] [ALPM] transaction started\n\ 38 | [2019-07-14 21:33] [ALPM] installed feh (3.1.3-1)\n\ 39 | [2019-07-14 21:33] [ALPM] upgraded libev (4.25-1 -> 4.27-1)\n\ 40 | [2019-07-14 21:33] [ALPM] upgraded iso-codes (4.2-1 -> 4.3-1)" 41 | ) 42 | .unwrap(); 43 | 44 | let args = vec![String::from("pkghist"), String::from("-l"), file_name]; 45 | let r = run(args); 46 | 47 | assert_eq!(r.is_ok(), true); 48 | 49 | fs::remove_file(file.path().unwrap()).unwrap() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: release 8 | 9 | jobs: 10 | release: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Install stable toolchain 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | override: true 23 | 24 | - name: Build release 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: build 28 | args: --release --all-features 29 | 30 | - name: Package release 31 | run: | 32 | tar cvfz pkghist.tar.gz completions/* -C target/release pkghist --numeric-owner 33 | 34 | - name: Create release 35 | id: create_release 36 | uses: actions/create-release@v1.0.0 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | tag_name: ${{ github.ref }} 41 | release_name: Release ${{ github.ref }} 42 | draft: false 43 | prerelease: false 44 | 45 | - name: Upload Release Asset 46 | id: upload-release-asset 47 | uses: actions/upload-release-asset@v1.0.1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | upload_url: ${{ steps.create_release.outputs.upload_url }} 52 | asset_path: pkghist.tar.gz 53 | asset_name: pkghist.tar.gz 54 | asset_content_type: application/octet-stream 55 | -------------------------------------------------------------------------------- /src/pacman/group.rs: -------------------------------------------------------------------------------- 1 | use crate::pacman::PacmanEvent; 2 | use std::collections::HashMap; 3 | 4 | pub trait Group { 5 | type Event; 6 | fn group(&self) -> HashMap<&String, Vec<&Self::Event>>; 7 | } 8 | 9 | impl Group for Vec { 10 | type Event = PacmanEvent; 11 | 12 | fn group(&self) -> HashMap<&String, Vec<&PacmanEvent>> { 13 | let mut groups: HashMap<&String, Vec<&PacmanEvent>> = HashMap::new(); 14 | for event in self { 15 | if groups.contains_key(&event.package) { 16 | let current_pacman_events: &Vec<&PacmanEvent> = groups.get(&event.package).unwrap(); 17 | let mut new_vec = Vec::from(current_pacman_events.as_slice()); 18 | new_vec.push(event); 19 | groups.insert(&event.package, new_vec); 20 | } else { 21 | let mut value = Vec::new(); 22 | value.push(event); 23 | groups.insert(&event.package, value); 24 | } 25 | } 26 | groups 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use super::*; 33 | use Group; 34 | 35 | #[test] 36 | fn should_group_relevant() { 37 | let p1: PacmanEvent = "[2019-01-01 00:00] [ALPM] installed a (1.0.0)" 38 | .parse() 39 | .unwrap(); 40 | let p2: PacmanEvent = "[2019-01-01 00:00] [ALPM] installed b (1.0.0)" 41 | .parse() 42 | .unwrap(); 43 | let p3: PacmanEvent = "[2019-01-02 00:00] [ALPM] upgraded b (1.0.1)" 44 | .parse() 45 | .unwrap(); 46 | let p4: PacmanEvent = "[2019-01-02 00:00] [ALPM] installed c (1.0.0)" 47 | .parse() 48 | .unwrap(); 49 | 50 | let pacman_events = [p1, p2, p3, p4].to_vec(); 51 | 52 | let groups = pacman_events.group(); 53 | assert_eq!(groups.keys().len(), 3) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /completions/_pkghist: -------------------------------------------------------------------------------- 1 | #compdef pkghist 2 | 3 | autoload -U is-at-least 4 | 5 | _pkghist() { 6 | typeset -A opt_args 7 | typeset -a _arguments_options 8 | local ret=1 9 | 10 | if is-at-least 5.2; then 11 | _arguments_options=(-s -S -C) 12 | else 13 | _arguments_options=(-s -C) 14 | fi 15 | 16 | local context curcontext="$curcontext" state line 17 | _arguments "${_arguments_options[@]}" \ 18 | '-o+[Select the output format]: :(json plain compact)' \ 19 | '--output-format=[Select the output format]: :(json plain compact)' \ 20 | '-l+[Specify a logfile]:FILE: ' \ 21 | '--logfile=[Specify a logfile]:FILE: ' \ 22 | '-L+[How many versions to go back in report. \[limit > 0\]]: : ' \ 23 | '--limit=[How many versions to go back in report. \[limit > 0\]]: : ' \ 24 | '(--last)--first=[Output the first '\''n'\'' pacman events]:n: ' \ 25 | '()--last=[Output the last '\''n'\'' pacman events]:n: ' \ 26 | '-a+[Only consider events that occurred after '\''date'\'' \[Format\: "YYYY-MM-DD HH\:MM"\]]:date: ' \ 27 | '--after=[Only consider events that occurred after '\''date'\'' \[Format\: "YYYY-MM-DD HH\:MM"\]]:date: ' \ 28 | '(-R --removed-only)-r[Include packages that are currently uninstalled]' \ 29 | '(-R --removed-only)--with-removed[Include packages that are currently uninstalled]' \ 30 | '(-r --with-removed)-R[Only output packages that are currently uninstalled]' \ 31 | '(-r --with-removed)--removed-only[Only output packages that are currently uninstalled]' \ 32 | '--no-colors[Disable colored output]' \ 33 | '--no-details[Only output the package names]' \ 34 | '-x[If set, every filter result will be excluded.]' \ 35 | '--exclude[If set, every filter result will be excluded.]' \ 36 | '-h[Print help]' \ 37 | '--help[Print help]' \ 38 | '-V[Print version]' \ 39 | '--version[Print version]' \ 40 | '*::filter -- Filter the packages that should be searched for. Use regular expressions to specify the exact pattern to match (e.g. '\''^linux$'\'' only matches the package '\''linux'\''):' \ 41 | && ret=0 42 | } 43 | 44 | (( $+functions[_pkghist_commands] )) || 45 | _pkghist_commands() { 46 | local commands; commands=() 47 | _describe -t commands 'pkghist commands' commands "$@" 48 | } 49 | 50 | if [ "$funcstack[1]" = "_pkghist" ]; then 51 | _pkghist "$@" 52 | else 53 | compdef _pkghist pkghist 54 | fi 55 | -------------------------------------------------------------------------------- /completions/pkghist.bash: -------------------------------------------------------------------------------- 1 | _pkghist() { 2 | local i cur prev opts cmd 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | cmd="" 7 | opts="" 8 | 9 | for i in ${COMP_WORDS[@]} 10 | do 11 | case "${cmd},${i}" in 12 | ",$1") 13 | cmd="pkghist" 14 | ;; 15 | *) 16 | ;; 17 | esac 18 | done 19 | 20 | case "${cmd}" in 21 | pkghist) 22 | opts="-o -l -r -R -L -a -x -h -V --output-format --logfile --with-removed --removed-only --limit --no-colors --no-details --first --last --after --exclude --help --version [filter]..." 23 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then 24 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 25 | return 0 26 | fi 27 | case "${prev}" in 28 | --output-format) 29 | COMPREPLY=($(compgen -W "json plain compact" -- "${cur}")) 30 | return 0 31 | ;; 32 | -o) 33 | COMPREPLY=($(compgen -W "json plain compact" -- "${cur}")) 34 | return 0 35 | ;; 36 | --logfile) 37 | COMPREPLY=($(compgen -f "${cur}")) 38 | return 0 39 | ;; 40 | -l) 41 | COMPREPLY=($(compgen -f "${cur}")) 42 | return 0 43 | ;; 44 | --limit) 45 | COMPREPLY=($(compgen -f "${cur}")) 46 | return 0 47 | ;; 48 | -L) 49 | COMPREPLY=($(compgen -f "${cur}")) 50 | return 0 51 | ;; 52 | --first) 53 | COMPREPLY=($(compgen -f "${cur}")) 54 | return 0 55 | ;; 56 | --last) 57 | COMPREPLY=($(compgen -f "${cur}")) 58 | return 0 59 | ;; 60 | --after) 61 | COMPREPLY=($(compgen -f "${cur}")) 62 | return 0 63 | ;; 64 | -a) 65 | COMPREPLY=($(compgen -f "${cur}")) 66 | return 0 67 | ;; 68 | *) 69 | COMPREPLY=() 70 | ;; 71 | esac 72 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 73 | return 0 74 | ;; 75 | esac 76 | } 77 | 78 | complete -F _pkghist -o bashdefault -o default pkghist 79 | -------------------------------------------------------------------------------- /src/pacman/action.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::error::{Error, ErrorDetail}; 4 | 5 | #[derive(Debug, Eq, PartialEq, PartialOrd, Clone)] 6 | pub enum Action { 7 | Installed, 8 | Reinstalled, 9 | Upgraded, 10 | Downgraded, 11 | Removed, 12 | } 13 | 14 | impl Action { 15 | pub fn is_removed(&self) -> bool { 16 | *self == Action::Removed 17 | } 18 | 19 | pub fn is_installed(&self) -> bool { 20 | !self.is_removed() 21 | } 22 | } 23 | 24 | impl std::fmt::Display for Action { 25 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 26 | write!(f, "{:?}", self) 27 | } 28 | } 29 | 30 | impl FromStr for Action { 31 | type Err = Error; 32 | 33 | fn from_str(s: &str) -> std::result::Result { 34 | match s.to_lowercase().as_str() { 35 | "upgraded" => Ok(Action::Upgraded), 36 | "downgraded" => Ok(Action::Downgraded), 37 | "installed" => Ok(Action::Installed), 38 | "reinstalled" => Ok(Action::Reinstalled), 39 | "removed" => Ok(Action::Removed), 40 | _ => Err(Error::new(ErrorDetail::InvalidAction)), 41 | } 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests { 47 | use super::*; 48 | 49 | #[test] 50 | fn should_be_removed() { 51 | let removed = Action::Removed; 52 | assert_eq!(removed.is_removed(), true); 53 | assert_eq!(removed.is_installed(), false) 54 | } 55 | 56 | #[test] 57 | fn should_be_installed() { 58 | let installed = Action::Upgraded; 59 | assert_eq!(installed.is_installed(), true); 60 | assert_eq!(installed.is_removed(), false) 61 | } 62 | 63 | #[test] 64 | fn should_parse_action_installed() { 65 | let action: Action = "installed".parse().unwrap(); 66 | assert_eq!(action, Action::Installed) 67 | } 68 | 69 | #[test] 70 | fn should_parse_action_reinstalled() { 71 | let action: Action = "reinstalled".parse().unwrap(); 72 | assert_eq!(action, Action::Reinstalled) 73 | } 74 | 75 | #[test] 76 | fn should_parse_action_downgraded() { 77 | let action: Action = "downgraded".parse().unwrap(); 78 | assert_eq!(action, Action::Downgraded) 79 | } 80 | 81 | #[test] 82 | fn should_parse_action_removed() { 83 | let action: Action = "removed".parse().unwrap(); 84 | assert_eq!(action, Action::Removed) 85 | } 86 | 87 | #[test] 88 | fn should_parse_action_upgraded() { 89 | let action: Action = "upgraded".parse().unwrap(); 90 | assert_eq!(action, Action::Upgraded) 91 | } 92 | 93 | #[test] 94 | fn should_not_parse_an_action() { 95 | let action: Error = Action::from_str("foo").err().unwrap(); 96 | assert_eq!(action, Error::new(ErrorDetail::InvalidAction)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum ErrorDetail { 5 | IOError { msg: String }, 6 | InvalidFormat, 7 | InvalidAction, 8 | FormattingError { msg: String }, 9 | } 10 | 11 | impl fmt::Display for ErrorDetail { 12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | write!(f, "{:?}", self) 14 | } 15 | } 16 | 17 | #[derive(Debug, PartialEq)] 18 | pub struct Error { 19 | detail: ErrorDetail, 20 | } 21 | 22 | impl fmt::Display for Error { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | write!(f, "Error: {}", &self.detail) 25 | } 26 | } 27 | 28 | impl Error { 29 | pub fn new(error_detail: ErrorDetail) -> Error { 30 | Error { 31 | detail: error_detail, 32 | } 33 | } 34 | } 35 | 36 | impl From for Error { 37 | fn from(error: std::fmt::Error) -> Self { 38 | Error::new(ErrorDetail::FormattingError { 39 | msg: error.to_string(), 40 | }) 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(error: std::io::Error) -> Self { 46 | Error::new(ErrorDetail::IOError { 47 | msg: format!("{:?} -> {}", error.kind(), error.to_string()), 48 | }) 49 | } 50 | } 51 | 52 | #[cfg(test)] 53 | mod tests { 54 | use super::*; 55 | use std::io::ErrorKind; 56 | 57 | #[test] 58 | fn should_convert_the_fmt_error() { 59 | let e = std::fmt::Error::default(); 60 | let error = Error::from(e); 61 | assert_eq!( 62 | error.detail, 63 | ErrorDetail::FormattingError { 64 | msg: String::from("an error occurred when formatting an argument") 65 | } 66 | ) 67 | } 68 | 69 | #[test] 70 | fn should_convert_the_io_error() { 71 | let e = std::io::Error::new(ErrorKind::BrokenPipe, "Pipe is broken"); 72 | let error = Error::from(e); 73 | assert_eq!( 74 | error.detail, 75 | ErrorDetail::IOError { 76 | msg: String::from("BrokenPipe -> Pipe is broken") 77 | } 78 | ) 79 | } 80 | 81 | #[test] 82 | fn should_set_the_error_message() { 83 | let error = Error::new(ErrorDetail::FormattingError { 84 | msg: String::from("This error is a test"), 85 | }); 86 | assert_eq!( 87 | error.to_string(), 88 | "Error: FormattingError { msg: \"This error is a test\" }" 89 | ) 90 | } 91 | 92 | #[test] 93 | fn should_set_the_error_detail() { 94 | let error = Error::new(ErrorDetail::InvalidFormat); 95 | assert_eq!(error.detail, ErrorDetail::InvalidFormat) 96 | } 97 | 98 | #[test] 99 | fn should_format_correctly() { 100 | let error = Error::new(ErrorDetail::InvalidFormat); 101 | let str = format!("{}", error); 102 | assert_eq!(str, String::from("Error: InvalidFormat")) 103 | } 104 | 105 | #[test] 106 | fn should_debug_format_correctly() { 107 | let error = Error::new(ErrorDetail::InvalidFormat); 108 | let str = format!("{:?}", error); 109 | assert_eq!(str, String::from("Error { detail: InvalidFormat }")) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/pacman/newest.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::pacman::PacmanEvent; 4 | use std::hash::BuildHasher; 5 | 6 | pub trait Newest { 7 | type Event; 8 | 9 | fn newest(&mut self) -> &Self::Event; 10 | } 11 | 12 | impl Newest for Vec<&PacmanEvent> { 13 | type Event = PacmanEvent; 14 | 15 | fn newest(&mut self) -> &PacmanEvent { 16 | self.sort(); 17 | self.last().unwrap() 18 | } 19 | } 20 | 21 | pub fn select_newest<'a, S: BuildHasher>( 22 | groups: HashMap<&'a String, Vec<&'a PacmanEvent>, S>, 23 | ) -> HashMap<&'a String, PacmanEvent> { 24 | let mut newest = HashMap::new(); 25 | for (package, mut pacman_events) in groups { 26 | let newest_event = pacman_events.newest().clone(); 27 | newest.insert(package, newest_event); 28 | } 29 | newest 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use Newest; 35 | 36 | use crate::pacman::PacmanEvent; 37 | 38 | use super::*; 39 | 40 | #[test] 41 | fn should_select_newest() { 42 | let p1: PacmanEvent = "[2019-05-23 07:00] [ALPM] installed intellij-idea (2:2019.1.2-1)" 43 | .parse() 44 | .unwrap(); 45 | let p2: PacmanEvent = 46 | "[2019-05-29 22:25] [ALPM] upgraded intellij-idea (2:2019.1.2-1 -> 2:2019.1.3-1)" 47 | .parse() 48 | .unwrap(); 49 | let p3: PacmanEvent = 50 | "[2019-07-25 01:17] [ALPM] upgraded intellij-idea (2:2019.1.3-1 -> 2:2019.1.3-2)" 51 | .parse() 52 | .unwrap(); 53 | let p4: PacmanEvent = 54 | "[2019-07-25 23:38] [ALPM] upgraded intellij-idea (2:2019.1.3-2 -> 2:2019.2-1)" 55 | .parse() 56 | .unwrap(); 57 | 58 | let mut pacman_events = [&p4, &p2, &p1, &p3].to_vec(); 59 | let latest = pacman_events.newest(); 60 | assert_eq!(latest, &p4) 61 | } 62 | 63 | #[test] 64 | fn should_select_newest_for_each_package() { 65 | let p1: PacmanEvent = "[2019-05-23 07:00] [ALPM] installed intellij-idea (2:2019.1.2-1)" 66 | .parse() 67 | .unwrap(); 68 | let p2: PacmanEvent = 69 | "[2019-05-29 22:25] [ALPM] upgraded intellij-idea (2:2019.1.2-1 -> 2:2019.1.3-1)" 70 | .parse() 71 | .unwrap(); 72 | let p3: PacmanEvent = 73 | "[2019-07-25 01:17] [ALPM] upgraded intellij-idea (2:2019.1.3-1 -> 2:2019.1.3-2)" 74 | .parse() 75 | .unwrap(); 76 | let p4: PacmanEvent = 77 | "[2019-07-25 23:38] [ALPM] upgraded intellij-idea (2:2019.1.3-2 -> 2:2019.2-1)" 78 | .parse() 79 | .unwrap(); 80 | 81 | let p5: PacmanEvent = 82 | "[2019-07-08 01:01] [ALPM] upgraded linux (5.1.15.arch1-1 -> 5.1.16.arch1-1)" 83 | .parse() 84 | .unwrap(); 85 | let p6: PacmanEvent = 86 | "[2019-07-11 22:08] [ALPM] upgraded linux (5.1.16.arch1-1 -> 5.2.arch2-1)" 87 | .parse() 88 | .unwrap(); 89 | let p7: PacmanEvent = 90 | "[2019-07-16 21:09] [ALPM] upgraded linux (5.2.arch2-1 -> 5.2.1.arch1-1)" 91 | .parse() 92 | .unwrap(); 93 | let p8: PacmanEvent = 94 | "[2019-07-25 01:16] [ALPM] upgraded linux (5.2.1.arch1-1 -> 5.2.2.arch1-1)" 95 | .parse() 96 | .unwrap(); 97 | 98 | let mut groups = HashMap::new(); 99 | let intellij_package = String::from("intellij-idea"); 100 | let intellij_events = [&p3, &p1, &p4, &p2].to_vec(); 101 | groups.insert(&intellij_package, intellij_events); 102 | 103 | let linux_package = String::from("linux"); 104 | let linux_events = [&p8, &p5, &p7, &p6].to_vec(); 105 | groups.insert(&linux_package, linux_events); 106 | 107 | let latest = select_newest(groups); 108 | assert_eq!(latest.get(&intellij_package), Some(&p4)); 109 | assert_eq!(latest.get(&linux_package), Some(&p8)) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/pacman/range.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use itertools::Itertools; 4 | 5 | use crate::opt::Direction; 6 | use crate::pacman::PacmanEvent; 7 | use std::hash::BuildHasher; 8 | 9 | pub fn range<'a, S: BuildHasher + Default>( 10 | grouped: &HashMap<&'a String, Vec<&'a PacmanEvent>, S>, 11 | direction: &Option, 12 | ) -> HashMap<&'a String, Vec<&'a PacmanEvent>> { 13 | let sorted: Vec<&String> = grouped 14 | .iter() 15 | .sorted_by(|(p1, e1), (p2, e2)| { 16 | let d1 = e1.last().unwrap().date; 17 | let d2 = e2.last().unwrap().date; 18 | if d1 == d2 { 19 | p1.cmp(p2) 20 | } else { 21 | d1.cmp(&d2) 22 | } 23 | }) 24 | .map(|(p, _)| *p) 25 | .unique() 26 | .collect(); 27 | 28 | let filters = match direction { 29 | Some(Direction::Forwards { n }) => sorted.into_iter().take(*n).collect(), 30 | Some(Direction::Backwards { n }) => sorted.into_iter().rev().take(*n).collect(), 31 | None => sorted, 32 | }; 33 | 34 | let mut filtered = HashMap::new(); 35 | grouped 36 | .iter() 37 | .filter(|(p, _)| filters.contains(*p)) 38 | .for_each(|(p, e)| { 39 | filtered.insert(*p, e.clone()); 40 | }); 41 | filtered 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; 47 | 48 | use crate::pacman::action::Action; 49 | use crate::pacman::group::Group; 50 | use crate::pacman::PacmanEvent; 51 | 52 | use super::*; 53 | 54 | fn some_pacman_events() -> Vec { 55 | let mut pacman_events = Vec::new(); 56 | pacman_events.push(PacmanEvent::new( 57 | NaiveDateTime::new( 58 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 59 | NaiveTime::from_hms_opt(11, 30, 0).unwrap(), 60 | ), 61 | Action::Installed, 62 | String::from("a"), 63 | String::from("0.0.1"), 64 | None, 65 | )); 66 | pacman_events.push(PacmanEvent::new( 67 | NaiveDateTime::new( 68 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 69 | NaiveTime::from_hms_opt(11, 30, 0).unwrap(), 70 | ), 71 | Action::Installed, 72 | String::from("b"), 73 | String::from("0.0.2"), 74 | None, 75 | )); 76 | pacman_events.push(PacmanEvent::new( 77 | NaiveDateTime::new( 78 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 79 | NaiveTime::from_hms_opt(12, 30, 0).unwrap(), 80 | ), 81 | Action::Installed, 82 | String::from("b"), 83 | String::from("0.0.2"), 84 | Some(String::from("0.0.3")), 85 | )); 86 | pacman_events.push(PacmanEvent::new( 87 | NaiveDateTime::new( 88 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 89 | NaiveTime::from_hms_opt(12, 31, 0).unwrap(), 90 | ), 91 | Action::Removed, 92 | String::from("b"), 93 | String::from("0.0.2"), 94 | None, 95 | )); 96 | pacman_events.push(PacmanEvent::new( 97 | NaiveDateTime::new( 98 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 99 | NaiveTime::from_hms_opt(12, 35, 0).unwrap(), 100 | ), 101 | Action::Installed, 102 | String::from("b"), 103 | String::from("0.0.2"), 104 | None, 105 | )); 106 | pacman_events.push(PacmanEvent::new( 107 | NaiveDateTime::new( 108 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 109 | NaiveTime::from_hms_opt(12, 35, 0).unwrap(), 110 | ), 111 | Action::Removed, 112 | String::from("c"), 113 | String::from("0.0.2"), 114 | None, 115 | )); 116 | pacman_events 117 | } 118 | 119 | #[test] 120 | fn should_get_last_n_packages() { 121 | // given 122 | let pacman_events = some_pacman_events(); 123 | let group = pacman_events.group(); 124 | 125 | // when 126 | let filtered = range(&group, &Some(Direction::Backwards { n: 2 })); 127 | 128 | // then 129 | assert_eq!(filtered.keys().len(), 2); 130 | assert!(filtered.get(&String::from("b")).is_some()); 131 | assert!(filtered.get(&String::from("c")).is_some()) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/opt/cli.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDateTime; 2 | use clap::{command, Arg, Command}; 3 | 4 | pub fn build_cli() -> Command { 5 | command!(env!("CARGO_PKG_NAME")) 6 | .version(env!("CARGO_PKG_VERSION")) 7 | .about("Trace package versions from pacman's logfile") 8 | .arg( 9 | Arg::new("output-format") 10 | .short('o') 11 | .long("output-format") 12 | .num_args(1) 13 | .value_parser(["json", "plain", "compact"]) 14 | .default_value("plain") 15 | .help("Select the output format"), 16 | ) 17 | .arg( 18 | Arg::new("logfile") 19 | .short('l') 20 | .long("logfile") 21 | .value_name("FILE") 22 | .help("Specify a logfile") 23 | .default_value("/var/log/pacman.log") 24 | .num_args(1), 25 | ) 26 | .arg( 27 | Arg::new("with-removed") 28 | .short('r') 29 | .long("with-removed") 30 | .num_args(0) 31 | .conflicts_with("removed-only") 32 | .help("Include packages that are currently uninstalled"), 33 | ) 34 | .arg( 35 | Arg::new("removed-only") 36 | .short('R') 37 | .long("removed-only") 38 | .num_args(0) 39 | .conflicts_with("with-removed") 40 | .help("Only output packages that are currently uninstalled"), 41 | ) 42 | .arg( 43 | Arg::new("limit") 44 | .help("How many versions to go back in report. [limit > 0]") 45 | .short('L') 46 | .long("limit") 47 | .num_args(1) 48 | .value_parser(validate_gt_0), 49 | ) 50 | .arg( 51 | Arg::new("no-colors") 52 | .num_args(0) 53 | .help("Disable colored output") 54 | .long("no-colors"), 55 | ) 56 | .arg( 57 | Arg::new("no-details") 58 | .num_args(0) 59 | .long("no-details") 60 | .help("Only output the package names") 61 | ) 62 | .arg( 63 | Arg::new("first") 64 | .long("first") 65 | .value_name("n") 66 | .num_args(1) 67 | .conflicts_with_all(&["filter", "last"]) 68 | .help("Output the first 'n' pacman events") 69 | .value_parser(validate_gt_0), 70 | ) 71 | .arg( 72 | Arg::new("last") 73 | .long("last") 74 | .value_name("n") 75 | .num_args(1) 76 | .conflicts_with("filter") 77 | .help("Output the last 'n' pacman events") 78 | .value_parser(validate_gt_0), 79 | ) 80 | .arg( 81 | Arg::new("after") 82 | .long("after") 83 | .short('a') 84 | .value_name("date") 85 | .help( 86 | "Only consider events that occurred after 'date' [Format: \"YYYY-MM-DD HH:MM\"]", 87 | ) 88 | .value_parser(validate_date) 89 | .num_args(1), 90 | ) 91 | .arg( 92 | Arg::new("exclude") 93 | .long("exclude") 94 | .short('x') 95 | .num_args(0) 96 | .help("If set, every filter result will be excluded.") 97 | ) 98 | .arg( 99 | Arg::new("filter") 100 | .help("Filter the packages that should be searched for. \ 101 | Use regular expressions to specify the exact pattern to match \ 102 | (e.g. '^linux$' only matches the package 'linux')") 103 | .num_args(0..), 104 | ) 105 | } 106 | 107 | fn validate_gt_0(str: &str) -> Result { 108 | match str.parse::() { 109 | Ok(l) => { 110 | if l > 0 { 111 | Ok(str.to_owned()) 112 | } else { 113 | Err(String::from("limit must be greater than 0")) 114 | } 115 | } 116 | Err(_) => Err(String::from("Please provide a positive number")), 117 | } 118 | } 119 | 120 | fn validate_date(str: &str) -> Result { 121 | match NaiveDateTime::parse_from_str(str, "%Y-%m-%d %H:%M") { 122 | Ok(_) => Ok(str.to_owned()), 123 | Err(_) => Err(String::from( 124 | "Please provide a date in the format \"YYYY-MM-DD HH:MM\"", 125 | )), 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use super::*; 132 | #[test] 133 | fn should_validate_gt_0() { 134 | let r = validate_gt_0("123"); 135 | assert_eq!(r.is_ok(), true) 136 | } 137 | 138 | #[test] 139 | fn should_not_validate_gt_0_no_number() { 140 | let r = validate_gt_0("notanumber"); 141 | assert_eq!(r.is_err(), true) 142 | } 143 | 144 | #[test] 145 | fn should_not_validate_gt_0() { 146 | let r = validate_gt_0("0"); 147 | assert_eq!(r.is_err(), true) 148 | } 149 | 150 | #[test] 151 | fn should_validate_date() { 152 | let d = validate_date("2019-10-02 12:30"); 153 | assert_eq!(d.is_ok(), true) 154 | } 155 | 156 | #[test] 157 | fn should_not_validate_date() { 158 | let d = validate_date("20191002 1230"); 159 | assert_eq!(d.is_err(), true) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/herzrasen/pkghist/branch/master/graph/badge.svg)](https://codecov.io/gh/herzrasen/pkghist) 2 | [![pkghist](https://img.shields.io/aur/version/pkghist.svg?label=pkghist)](https://aur.archlinux.org/packages/pkghist/) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/herzrasen/pkghist/blob/master/LICENSE) 4 | 5 | # pkghist 6 | `pkghist` queries your `Pacman` logfile for the local history of either single packages or your entire installation. 7 | 8 | ## About 9 | `pkghist` parses Pacman's logfile (usually located under `/var/log/pacman.log` or specified via `--logfile /path/to/pacman.log`) and outputs version information. 10 | `pkghist` can list information about currently uninstalled packages using the `--removed-only` or `--with-removed` options. 11 | 12 | ## Install 13 | If you are on Arch, either install `pkghist` using AUR or by building it using makepkg 14 | 15 | ```bash 16 | git clone https://aur.archlinux.org/pkghist.git 17 | cd pkghist 18 | makepkg -si 19 | ``` 20 | ## Build 21 | To build `pkghist` from source you need a `Rust` installation including it's build tool `Cargo`. 22 | Install it either using pacman or [follow the official install guide](https://www.rust-lang.org/tools/install). 23 | 24 | Once `Rust` and `Cargo` are up and running, simply use: 25 | ```bash 26 | cargo install --path . 27 | ``` 28 | 29 | This will build `pkghist` and install it into your cargo bin directory (usually `~/.cargo/bin`). 30 | 31 | ## Help 32 | ```bash 33 | pkghist 0.6.0 34 | Trace package versions from pacman's logfile 35 | 36 | USAGE: 37 | pkghist [FLAGS] [OPTIONS] [filter]... 38 | 39 | FLAGS: 40 | -x, --exclude If set, every filter result will be excluded. 41 | -h, --help Prints help information 42 | --no-colors Disable colored output 43 | --no-details Only output the package names 44 | -R, --removed-only Only output packages that are currently uninstalled 45 | -V, --version Prints version information 46 | -r, --with-removed Include packages that are currently uninstalled 47 | 48 | OPTIONS: 49 | -a, --after Only consider events that occurred after 'date' [Format: "YYYY-MM-DD HH:MM"] 50 | --first Output the first 'n' pacman events 51 | --last Output the last 'n' pacman events 52 | -L, --limit How many versions to go back in report. [limit > 0] 53 | -l, --logfile Specify a logfile [default: /var/log/pacman.log] 54 | -o, --output-format Select the output format [default: plain] [possible values: json, plain, 55 | compact] 56 | 57 | ARGS: 58 | ... Filter the packages that should be searched for. Use regular expressions to specify the exact 59 | pattern to match (e.g. '^linux$' only matches the package 'linux') 60 | ``` 61 | 62 | ## Usage 63 | ### List all installed packages ordered by install/upgrade date 64 | ```bash 65 | pkghist 66 | ``` 67 | 68 | ### List the last `n` installed / upgraded packages 69 | ```bash 70 | pkghist --last 71 | ``` 72 | 73 | ### Limit the number of versions per package 74 | ```bash 75 | pkghist --limit 76 | ``` 77 | 78 | ### Search for a package by exact name 79 | ```bash 80 | pkghist '^name$' 81 | ``` 82 | 83 | This uses regex syntax to describe the pattern to search for. 84 | 85 | #### Example 86 | ```bash 87 | pkghist '^zsh$' 88 | ``` 89 | This return only the package `zsh` and not for example `zsh-syntax-highlighting`. 90 | 91 | ### Search for all packages containing some string 92 | ```bash 93 | pkghist somestring 94 | ``` 95 | 96 | #### Example 97 | ```bash 98 | pkghist zsh 99 | ``` 100 | This returns the package `zsh` as well as for example `zsh-syntax-highlighting`. 101 | 102 | ### Excluding packages 103 | ```bash 104 | pkghist --exclude somestring 105 | ``` 106 | 107 | #### Example 108 | ```bash 109 | pkghist --exclude '^[a-e]' 110 | ``` 111 | This excludes all packages starting with the letters a to e. 112 | 113 | ### List the package names of all removed packages 114 | ```bash 115 | pkghist --no-details --removed-only 116 | ``` 117 | 118 | ## Regex examples 119 | This is a little collection of useful regexes that can be used for filtering. 120 | 121 | | Regex | Explanation | 122 | |----------------|----------------------------------------------------------| 123 | | `'^package$'` | Matches only packages named 'package' | 124 | | `'package'` | Matches any package containing 'package' | 125 | | `'^[a-x]'` | Matches any package starting with the letters 'a' to 'x' | 126 | | `'[a-x]'` | Matches any package containing the letter 'a' to 'x' | 127 | | `'[^a-x]'` | Matches any package NOT containing the letters 'a' to 'x'| 128 | | `'[[:digit:]]'`| Matches any package containing a digit | 129 | 130 | Sometimes using `--exclude` is easier than trying to create an exclusion regex. 131 | 132 | ## Using pkghist's output as input for pacman 133 | You can use the result of a `pkghist` query as input for pacman. 134 | 135 | To create an output in the matching format, use the `--no-colors` and the `--no-details' options in your query. 136 | 137 | The following command installs all packages that have been removed after 2019-10-01 12:00. 138 | ```bash 139 | sudo pacman -S $(pkghist --no-details --no-colors --removed-only --after "2019-10-01 12:00") 140 | ``` 141 | 142 | The following command removes all packages that have been installed after 2019-10-02 12:00. 143 | ```bash 144 | sudo pacman -R $(pkghist --no-details --no-colors --after "2019-10-02 12:00") 145 | ``` 146 | 147 | ## Shell completions 148 | `pkghist` creates completion scripts for `bash`, `fish` and `zsh`. 149 | They are created at build time using the great [clap crate](https://github.com/clap-rs/clap). 150 | When installing using `makepkg` (e.g. using the AUR), they are put into the appropriate location. 151 | When installing manually, you may copy them from [the completions directory](./completions) into the appropriate location. 152 | 153 | Note: When using zsh, enable loading of additional completions by adding the following line to your `.zshrc` 154 | ```bash 155 | autoload -U compinit && compinit 156 | ``` 157 | -------------------------------------------------------------------------------- /src/pkghist/mod.rs: -------------------------------------------------------------------------------- 1 | mod format; 2 | 3 | use std::path::Path; 4 | 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | 8 | use crate::error::Error; 9 | use crate::opt::Config; 10 | use crate::pacman; 11 | use crate::pacman::filter::Filter; 12 | use crate::pacman::PacmanEvent; 13 | use itertools::Itertools; 14 | use std::io::stdout; 15 | 16 | use crate::pkghist::format::Printer; 17 | 18 | pub fn run(config: Config) -> Result<(), Error> { 19 | let logfile_path = &config.logfile; 20 | let pacman_events = pacman::from_file(Path::new(logfile_path)).unwrap_or_else(|_| { 21 | eprintln!("Unable to open {}", logfile_path); 22 | std::process::exit(2) 23 | }); 24 | 25 | let groups = pacman_events.filter_packages(&config); 26 | 27 | let mut package_histories = Vec::new(); 28 | 29 | let sorted: Vec> = groups 30 | .iter() 31 | .sorted_by(|(p1, e1), (p2, e2)| { 32 | let d1 = e1.last().unwrap().date; 33 | let d2 = e2.last().unwrap().date; 34 | if d1 == d2 { 35 | p1.cmp(p2) 36 | } else { 37 | d1.cmp(&d2) 38 | } 39 | }) 40 | .map(|(_, e)| e.clone()) 41 | .collect(); 42 | 43 | for mut events in sorted { 44 | events.sort(); 45 | let package_history = PackageHistory::from_pacman_events(events); 46 | package_histories.push(package_history); 47 | } 48 | 49 | match config.format.print(&mut stdout(), &package_histories) { 50 | _ => Ok(()), 51 | } 52 | } 53 | 54 | #[derive(Debug, Serialize, Deserialize, Clone, Ord, Eq, PartialOrd, PartialEq)] 55 | pub struct Event { 56 | pub v: String, 57 | pub d: String, 58 | pub a: String, 59 | } 60 | 61 | impl Event { 62 | fn new(version: String, date: String, action: String) -> Event { 63 | Event { 64 | v: version, 65 | d: date, 66 | a: action, 67 | } 68 | } 69 | 70 | fn from_pacman_event(pacman_event: &PacmanEvent) -> Event { 71 | Event::new( 72 | pacman_event.printable_version(), 73 | pacman_event.date.to_string(), 74 | pacman_event.action.to_string(), 75 | ) 76 | } 77 | } 78 | 79 | #[derive(Debug, Serialize, Deserialize, Clone)] 80 | pub struct PackageHistory { 81 | pub p: String, 82 | pub e: Vec, 83 | } 84 | 85 | impl PackageHistory { 86 | fn new(p: String, e: Vec) -> PackageHistory { 87 | PackageHistory { p, e } 88 | } 89 | 90 | fn from_pacman_events(pacman_events: Vec<&PacmanEvent>) -> PackageHistory { 91 | let e: Vec = pacman_events 92 | .iter() 93 | .map(|e| Event::from_pacman_event(e)) 94 | .collect(); 95 | let p = pacman_events.first().unwrap().package.clone(); 96 | PackageHistory::new(p, e) 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use std::fs; 103 | use std::fs::File; 104 | use std::io::Write; 105 | 106 | use filepath::FilePath; 107 | 108 | use crate::pacman::action::Action; 109 | 110 | use super::*; 111 | use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; 112 | 113 | #[test] 114 | fn should_create_package_histories_with_new() { 115 | let ev1 = Event::new( 116 | String::from("1.2.1"), 117 | String::from("2019-10-01 12:30:00"), 118 | String::from("Upgraded"), 119 | ); 120 | let ev2 = Event::new( 121 | String::from("1.2.1"), 122 | String::from("2019-10-01 13:30:00"), 123 | String::from("Removed"), 124 | ); 125 | 126 | let package_histories = 127 | PackageHistory::new(String::from("foo"), vec![ev1.clone(), ev2.clone()]); 128 | 129 | assert_eq!(package_histories.p, "foo"); 130 | assert_eq!(package_histories.e.len(), 2); 131 | assert!(package_histories.e.contains(&ev1)); 132 | assert!(package_histories.e.contains(&ev2)) 133 | } 134 | 135 | #[test] 136 | fn should_create_package_histories() { 137 | let ev1 = PacmanEvent::new( 138 | NaiveDateTime::new( 139 | NaiveDate::from_ymd_opt(2019, 9, 1).unwrap(), 140 | NaiveTime::from_hms_opt(12, 30, 0).unwrap(), 141 | ), 142 | Action::Installed, 143 | String::from("test"), 144 | String::from("0.1.0"), 145 | None, 146 | ); 147 | let ev2 = PacmanEvent::new( 148 | NaiveDateTime::new( 149 | NaiveDate::from_ymd_opt(2019, 9, 1).unwrap(), 150 | NaiveTime::from_hms_opt(18, 30, 10).unwrap(), 151 | ), 152 | Action::Upgraded, 153 | String::from("test"), 154 | String::from("0.1.0"), 155 | Some(String::from("0.1.1")), 156 | ); 157 | 158 | let pacman_events = vec![&ev1, &ev2]; 159 | let package_history = PackageHistory::from_pacman_events(pacman_events); 160 | assert_eq!(package_history.p, "test"); 161 | assert_eq!(package_history.e.len(), 2); 162 | assert_eq!( 163 | package_history.e, 164 | vec![ 165 | Event::new( 166 | String::from("0.1.0"), 167 | String::from("2019-09-01 12:30:00"), 168 | String::from("Installed") 169 | ), 170 | Event::new( 171 | String::from("0.1.1"), 172 | String::from("2019-09-01 18:30:10"), 173 | String::from("Upgraded") 174 | ) 175 | ] 176 | ) 177 | } 178 | 179 | #[test] 180 | fn should_be_ok_1() { 181 | let file_name = uuid::Uuid::new_v4().to_string(); 182 | let mut file = File::create(&file_name).unwrap(); 183 | writeln!( 184 | file, 185 | "[2019-07-14 21:33] [PACMAN] synchronizing package lists\n\ 186 | [2019-07-14 21:33] [PACMAN] starting full system upgrade\n\ 187 | [2019-07-14 21:33] [ALPM] transaction started\n\ 188 | [2019-07-14 21:33] [ALPM] installed feh (3.1.3-1)\n\ 189 | [2019-07-14 21:33] [ALPM] upgraded libev (4.25-1 -> 4.27-1)\n\ 190 | [2019-07-14 21:33] [ALPM] upgraded iso-codes (4.2-1 -> 4.3-1)" 191 | ) 192 | .unwrap(); 193 | 194 | let mut config = Config::new(); 195 | config.logfile = file_name; 196 | 197 | let result = run(config); 198 | assert_eq!(result.is_ok(), true); 199 | fs::remove_file(file.path().unwrap()).unwrap() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/pacman/mod.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::fs::File; 3 | use std::io::{BufRead, BufReader}; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | 7 | use chrono::DateTime; 8 | use chrono::NaiveDateTime; 9 | 10 | use lazy_static::*; 11 | use regex::*; 12 | 13 | use crate::error::{Error, ErrorDetail}; 14 | use crate::pacman::action::Action; 15 | 16 | pub mod action; 17 | pub mod filter; 18 | pub mod group; 19 | pub mod newest; 20 | pub mod range; 21 | 22 | lazy_static! { 23 | static ref REGEX: Regex = Regex::new(r"^\[(?P(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{4}))\]\s\[.+\]\s(?Pupgraded|installed|removed|reinstalled|downgraded)\s(?P.+)\s\((?P.+?)(\s->\s(?P.+))?\)").unwrap(); 24 | } 25 | 26 | #[derive(Debug, Eq, PartialEq, Clone)] 27 | pub struct PacmanEvent { 28 | pub date: NaiveDateTime, 29 | pub action: Action, 30 | pub package: String, 31 | pub from: String, 32 | pub to: Option, 33 | } 34 | 35 | impl PacmanEvent { 36 | pub fn new( 37 | date: NaiveDateTime, 38 | action: Action, 39 | package: String, 40 | from: String, 41 | to: Option, 42 | ) -> PacmanEvent { 43 | PacmanEvent { 44 | date, 45 | action, 46 | package, 47 | from, 48 | to, 49 | } 50 | } 51 | 52 | pub fn printable_version(&self) -> String { 53 | if self.to.is_some() { 54 | self.to.clone().unwrap() 55 | } else { 56 | self.from.clone() 57 | } 58 | } 59 | } 60 | 61 | impl Ord for PacmanEvent { 62 | fn cmp(&self, other: &Self) -> Ordering { 63 | if self.package == other.package { 64 | self.date.cmp(&other.date) 65 | } else { 66 | self.package.cmp(&other.package) 67 | } 68 | } 69 | } 70 | 71 | impl PartialOrd for PacmanEvent { 72 | fn partial_cmp(&self, other: &Self) -> Option { 73 | Some(self.cmp(other)) 74 | } 75 | } 76 | 77 | impl FromStr for PacmanEvent { 78 | type Err = Error; 79 | 80 | fn from_str(s: &str) -> Result { 81 | if REGEX.is_match(s) { 82 | match REGEX.captures(s) { 83 | Some(captures) => { 84 | let date = parse_date(captures.name("date").unwrap().as_str()); 85 | let action = 86 | Action::from_str(captures.name("action").unwrap().as_str()).unwrap(); 87 | let package = String::from(captures.name("package").unwrap().as_str()); 88 | let from = String::from(captures.name("from").unwrap().as_str()); 89 | let to = match captures.name("to") { 90 | Some(to) => Some(String::from(to.as_str())), 91 | None => None, 92 | }; 93 | Ok(PacmanEvent::new(date, action, package, from, to)) 94 | } 95 | None => Err(Error::new(ErrorDetail::InvalidFormat)), 96 | } 97 | } else { 98 | Err(Error::new(ErrorDetail::InvalidFormat)) 99 | } 100 | } 101 | } 102 | 103 | fn parse_date(date_str: &str) -> NaiveDateTime { 104 | let d = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M"); 105 | match d { 106 | Ok(n) => n, 107 | Err(_) => { 108 | let date_time = DateTime::parse_from_str(date_str, "%Y-%m-%dT%H:%M:%S%z").unwrap(); 109 | date_time.naive_local() 110 | } 111 | } 112 | } 113 | 114 | pub fn from_file(path: &Path) -> std::io::Result> { 115 | let f = File::open(path)?; 116 | let file = BufReader::new(&f); 117 | let pacman_events: Vec = 118 | file.lines() 119 | .enumerate() 120 | .fold(vec![], |mut current, (idx, l)| match l { 121 | Ok(line) => match PacmanEvent::from_str(line.as_str()) { 122 | Ok(pacman_event) => { 123 | current.push(pacman_event); 124 | current 125 | } 126 | Err(_) => current, 127 | }, 128 | Err(e) => { 129 | eprintln!("Skipping line #{} ({e})", idx + 1); 130 | current 131 | } 132 | }); 133 | Ok(pacman_events) 134 | } 135 | 136 | #[cfg(test)] 137 | mod tests { 138 | use std::fs; 139 | 140 | use std::io::Write; 141 | use std::path; 142 | 143 | use chrono::{NaiveDate, NaiveTime}; 144 | use filepath::FilePath; 145 | 146 | use super::*; 147 | 148 | #[test] 149 | fn should_parse_new_date_format() { 150 | let p1: PacmanEvent = "[2019-10-23T20:25:18+0200] [ALPM] installed yay (9.4.2-1)" 151 | .parse() 152 | .unwrap(); 153 | assert_eq!( 154 | p1.date, 155 | NaiveDateTime::new( 156 | NaiveDate::from_ymd_opt(2019, 10, 23).unwrap(), 157 | NaiveTime::from_hms_opt(20, 25, 18).unwrap() 158 | ) 159 | ) 160 | } 161 | 162 | #[test] 163 | fn should_order_pacman_events_by_date() { 164 | let p1: PacmanEvent = "[2019-07-16 21:07] [ALPM] installed nvidia (430.26)" 165 | .parse() 166 | .unwrap(); 167 | let p2: PacmanEvent = "[2019-07-16 21:08] [ALPM] upgraded nvidia (430.26 -> 430.26-5)" 168 | .parse() 169 | .unwrap(); 170 | let p3: PacmanEvent = "[2019-07-16 21:09] [ALPM] upgraded nvidia (430.26-9 -> 430.26-10)" 171 | .parse() 172 | .unwrap(); 173 | let mut p = [&p2, &p3, &p1].to_vec(); 174 | 175 | p.sort(); 176 | 177 | let should_match = [&p1, &p2, &p3]; 178 | assert_eq!(p.as_slice(), should_match) 179 | } 180 | 181 | #[test] 182 | fn should_order_pacman_events_by_date_and_package() { 183 | let p1: PacmanEvent = 184 | "[2019-05-23 07:00] [ALPM] installed intellij-idea-community-edition (2:2019.1.2-1)" 185 | .parse() 186 | .unwrap(); 187 | let p2: PacmanEvent = "[2019-05-29 22:25] [ALPM] upgraded intellij-idea-community-edition (2:2019.1.2-1 -> 2:2019.1.3-1)".parse().unwrap(); 188 | let p3: PacmanEvent = "[2019-07-25 01:17] [ALPM] upgraded intellij-idea-community-edition (2:2019.1.3-1 -> 2:2019.1.3-2)".parse().unwrap(); 189 | let p4: PacmanEvent = "[2019-07-25 23:38] [ALPM] upgraded intellij-idea-community-edition (2:2019.1.3-2 -> 2:2019.2-1)".parse().unwrap(); 190 | 191 | let p5: PacmanEvent = 192 | "[2019-07-08 01:01] [ALPM] upgraded linux (5.1.15.arch1-1 -> 5.1.16.arch1-1)" 193 | .parse() 194 | .unwrap(); 195 | let p6: PacmanEvent = 196 | "[2019-07-11 22:08] [ALPM] upgraded linux (5.1.16.arch1-1 -> 5.2.arch2-1)" 197 | .parse() 198 | .unwrap(); 199 | let p7: PacmanEvent = 200 | "[2019-07-16 21:09] [ALPM] upgraded linux (5.2.arch2-1 -> 5.2.1.arch1-1)" 201 | .parse() 202 | .unwrap(); 203 | let p8: PacmanEvent = 204 | "[2019-07-25 01:16] [ALPM] upgraded linux (5.2.1.arch1-1 -> 5.2.2.arch1-1)" 205 | .parse() 206 | .unwrap(); 207 | 208 | let mut p = [&p5, &p3, &p8, &p6, &p1, &p4, &p2, &p7].to_vec(); 209 | 210 | p.sort(); 211 | 212 | let should_match = [&p1, &p2, &p3, &p4, &p5, &p6, &p7, &p8]; 213 | assert_eq!(p.as_slice(), should_match) 214 | } 215 | 216 | #[test] 217 | fn should_extract_a_pacman_event_with_from_and_to() { 218 | let line: PacmanEvent = "[2019-07-05 22:10] [ALPM] upgraded libva (2.4.1-1 -> 2.5.0-1)" 219 | .parse() 220 | .unwrap(); 221 | let expected_pacman_event = PacmanEvent { 222 | date: NaiveDateTime::new( 223 | NaiveDate::from_ymd_opt(2019, 7, 5).unwrap(), 224 | NaiveTime::from_hms_opt(22, 10, 0).unwrap(), 225 | ), 226 | action: Action::Upgraded, 227 | package: String::from("libva"), 228 | from: String::from("2.4.1-1"), 229 | to: Some(String::from("2.5.0-1")), 230 | }; 231 | assert_eq!(line, expected_pacman_event) 232 | } 233 | 234 | #[test] 235 | fn should_extract_a_pacman_install_event() { 236 | let line: PacmanEvent = "[2019-06-26 10:47] [ALPM] installed ansible (2.8.1-1)" 237 | .parse() 238 | .unwrap(); 239 | let exptected_pacman_event = PacmanEvent { 240 | date: NaiveDateTime::new( 241 | NaiveDate::from_ymd_opt(2019, 6, 26).unwrap(), 242 | NaiveTime::from_hms_opt(10, 47, 0).unwrap(), 243 | ), 244 | action: Action::Installed, 245 | package: String::from("ansible"), 246 | from: String::from("2.8.1-1"), 247 | to: None, 248 | }; 249 | assert_eq!(line, exptected_pacman_event) 250 | } 251 | 252 | #[test] 253 | fn should_extract_a_pacman_downgraded_event() { 254 | let line: PacmanEvent = 255 | "[2018-12-15 00:22] [ALPM] downgraded mps-youtube (0.2.8-2 -> 0.2.8-1)" 256 | .parse() 257 | .unwrap(); 258 | let expected_pacman_event = PacmanEvent { 259 | date: NaiveDateTime::new( 260 | NaiveDate::from_ymd_opt(2018, 12, 15).unwrap(), 261 | NaiveTime::from_hms_opt(0, 22, 0).unwrap(), 262 | ), 263 | action: Action::Downgraded, 264 | package: String::from("mps-youtube"), 265 | from: String::from("0.2.8-2"), 266 | to: Some(String::from("0.2.8-1")), 267 | }; 268 | assert_eq!(line, expected_pacman_event) 269 | } 270 | 271 | #[test] 272 | fn should_extract_a_pacman_reinstall_event() { 273 | let line: PacmanEvent = "[2019-06-26 10:47] [ALPM] reinstalled ansible (2.8.1-1)" 274 | .parse() 275 | .unwrap(); 276 | let exptected_pacman_event = PacmanEvent::new( 277 | NaiveDateTime::new( 278 | NaiveDate::from_ymd_opt(2019, 6, 26).unwrap(), 279 | NaiveTime::from_hms_opt(10, 47, 0).unwrap(), 280 | ), 281 | Action::Reinstalled, 282 | String::from("ansible"), 283 | String::from("2.8.1-1"), 284 | None, 285 | ); 286 | assert_eq!(line, exptected_pacman_event) 287 | } 288 | 289 | #[test] 290 | fn should_extract_a_removed_pacman_event_with_from() { 291 | let line: PacmanEvent = "[2019-07-04 14:05] [ALPM] removed gnome-common (3.18.0-3)" 292 | .parse() 293 | .unwrap(); 294 | let expected_pacman_event = PacmanEvent::new( 295 | NaiveDateTime::new( 296 | NaiveDate::from_ymd_opt(2019, 7, 4).unwrap(), 297 | NaiveTime::from_hms_opt(14, 5, 0).unwrap(), 298 | ), 299 | Action::Removed, 300 | String::from("gnome-common"), 301 | String::from("3.18.0-3"), 302 | None, 303 | ); 304 | assert_eq!(line, expected_pacman_event) 305 | } 306 | 307 | #[test] 308 | fn should_not_extract_a_pacman_event() { 309 | let r = PacmanEvent::from_str("[2019-07-04 14:05] I AM NOT MATCHING"); 310 | assert_eq!(r.is_err(), true) 311 | } 312 | 313 | #[test] 314 | fn should_result_in_an_error() { 315 | let res = from_file(path::Path::new(&String::from("/not/found"))); 316 | assert_eq!(res.is_err(), true) 317 | } 318 | 319 | #[test] 320 | fn should_extract_the_valid_lines() { 321 | let mut file = File::create(uuid::Uuid::new_v4().to_string()).unwrap(); 322 | writeln!( 323 | file, 324 | "[2019-07-14 21:33] [PACMAN] synchronizing package lists\n[2019-07-14 21:33] [PACMAN] starting full system upgrade\n[2019-07-14 21:33] [ALPM] transaction started\n[2019-07-14 21:33] [ALPM] upgraded feh (3.1.3-1 -> 3.2-1)\n[2019-07-14 21:33] [ALPM] upgraded libev (4.25-1 -> 4.27-1)\n[2019-07-14 21:33] [ALPM] upgraded iso-codes (4.2-1 -> 4.3-1)" 325 | ) 326 | .unwrap(); 327 | 328 | let pacman_events = from_file(&file.path().unwrap()).unwrap(); 329 | 330 | assert_eq!(pacman_events.len(), 3); 331 | 332 | let packages: Vec = pacman_events.iter().map(|p| p.package.clone()).collect(); 333 | assert_eq!( 334 | packages.as_slice(), 335 | [ 336 | String::from("feh"), 337 | String::from("libev"), 338 | String::from("iso-codes"), 339 | ] 340 | ); 341 | fs::remove_file(file.path().unwrap()).unwrap() 342 | } 343 | 344 | #[test] 345 | fn should_skip_invalid_line() { 346 | let file_name = uuid::Uuid::new_v4().to_string(); 347 | let mut file = File::create(&file_name).unwrap(); 348 | writeln!( 349 | file, 350 | "[2018-12-15 00:19] [PACMAN] Running 'pacman -U ^��sA��'" 351 | ) 352 | .unwrap(); 353 | 354 | let pacman_events = from_file(Path::new(&file_name)).unwrap(); 355 | 356 | assert_eq!(pacman_events.len(), 0); 357 | 358 | fs::remove_file(file.path().unwrap()).unwrap() 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/opt/mod.rs: -------------------------------------------------------------------------------- 1 | use std::println; 2 | use std::str::FromStr; 3 | 4 | use crate::error::Error; 5 | use crate::error::ErrorDetail; 6 | 7 | use chrono::NaiveDateTime; 8 | use clap::ArgMatches; 9 | 10 | use regex::Regex; 11 | 12 | pub mod cli; 13 | 14 | pub fn parse_args<'a>(argv: &[String]) -> ArgMatches { 15 | cli::build_cli().get_matches_from(argv) 16 | } 17 | 18 | #[derive(Debug, PartialOrd, PartialEq)] 19 | pub enum Format { 20 | Plain { 21 | with_colors: bool, 22 | without_details: bool, 23 | }, 24 | Json { 25 | without_details: bool, 26 | }, 27 | Compact { 28 | with_colors: bool, 29 | without_details: bool, 30 | }, 31 | } 32 | 33 | impl FromStr for Format { 34 | type Err = Error; 35 | 36 | fn from_str(s: &str) -> Result { 37 | let format_str = s.to_lowercase(); 38 | if format_str == "json" { 39 | Ok(Format::Json { 40 | without_details: false, 41 | }) 42 | } else if format_str == "plain" { 43 | Ok(Format::Plain { 44 | with_colors: true, 45 | without_details: false, 46 | }) 47 | } else if format_str == "compact" { 48 | Ok(Format::Compact { 49 | with_colors: true, 50 | without_details: false, 51 | }) 52 | } else { 53 | Err(Error::new(ErrorDetail::InvalidFormat)) 54 | } 55 | } 56 | } 57 | 58 | #[derive(Debug, PartialOrd, PartialEq)] 59 | pub enum Direction { 60 | Forwards { n: usize }, 61 | Backwards { n: usize }, 62 | } 63 | 64 | impl Direction { 65 | fn from_first(n: u32) -> Direction { 66 | Direction::Forwards { n: n as usize } 67 | } 68 | 69 | fn from_last(n: u32) -> Direction { 70 | Direction::Backwards { n: n as usize } 71 | } 72 | } 73 | 74 | #[derive(Debug)] 75 | pub struct Config { 76 | pub exclude: bool, 77 | pub removed_only: bool, 78 | pub with_removed: bool, 79 | pub logfile: String, 80 | pub filters: Vec, 81 | pub format: Format, 82 | pub limit: Option, 83 | pub direction: Option, 84 | pub after: Option, 85 | } 86 | 87 | impl Default for Config { 88 | fn default() -> Config { 89 | Config { 90 | exclude: false, 91 | removed_only: false, 92 | with_removed: false, 93 | logfile: String::from("/var/log/pacman.log"), 94 | format: Format::Plain { 95 | with_colors: true, 96 | without_details: false, 97 | }, 98 | limit: None, 99 | direction: None, 100 | after: None, 101 | filters: Vec::new(), 102 | } 103 | } 104 | } 105 | 106 | impl Config { 107 | pub fn new() -> Config { 108 | Default::default() 109 | } 110 | 111 | pub fn from_arg_matches(matches: &ArgMatches) -> Config { 112 | let filters = match matches.get_many::("filter") { 113 | Some(filters) => filters.fold(Vec::new(), |mut current, f| { 114 | println!("{}", f); 115 | let r = Regex::new(f).unwrap(); 116 | current.push(r); 117 | current 118 | }), 119 | None => Vec::new(), 120 | }; 121 | 122 | let with_colors = !matches.get_flag("no-colors"); 123 | 124 | let without_details = matches.get_flag("no-details"); 125 | 126 | let format = match matches 127 | .get_one::("output-format") 128 | .unwrap() 129 | .parse() 130 | .unwrap() 131 | { 132 | Format::Plain { .. } => Format::Plain { 133 | with_colors, 134 | without_details, 135 | }, 136 | Format::Compact { .. } => Format::Compact { 137 | with_colors, 138 | without_details, 139 | }, 140 | Format::Json { .. } => Format::Json { without_details }, 141 | }; 142 | 143 | let limit = match matches.get_one::("limit") { 144 | Some(all) if all == "all" => None, 145 | Some(v) => Some(v.parse::().unwrap()), 146 | None => None, 147 | }; 148 | 149 | let direction = if matches.contains_id("first") { 150 | Some(Direction::from_first( 151 | matches.get_one::("first").unwrap().parse().unwrap(), 152 | )) 153 | } else if matches.contains_id("last") { 154 | Some(Direction::from_last( 155 | matches.get_one::("last").unwrap().parse().unwrap(), 156 | )) 157 | } else { 158 | None 159 | }; 160 | 161 | let after = match matches.get_one::("after") { 162 | Some(date_str) => { 163 | Some(NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M").unwrap()) 164 | } 165 | None => None, 166 | }; 167 | 168 | Config { 169 | exclude: matches.get_flag("exclude"), 170 | removed_only: matches.get_flag("removed-only"), 171 | with_removed: matches.get_flag("with-removed"), 172 | logfile: matches.get_one::("logfile").unwrap().to_owned(), 173 | limit, 174 | filters, 175 | format, 176 | direction, 177 | after, 178 | } 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | mod tests { 184 | use std::println; 185 | 186 | use super::*; 187 | use chrono::{NaiveDate, NaiveTime}; 188 | 189 | #[test] 190 | fn should_parse_format_plain() { 191 | let format: Result = "plain".parse(); 192 | assert!(format.is_ok()); 193 | assert_eq!( 194 | format.unwrap(), 195 | Format::Plain { 196 | with_colors: true, 197 | without_details: false 198 | } 199 | ) 200 | } 201 | 202 | #[test] 203 | fn should_parse_format_plain_ignore_case() { 204 | let format: Result = "PlAiN".parse(); 205 | assert!(format.is_ok()); 206 | assert_eq!( 207 | format.unwrap(), 208 | Format::Plain { 209 | with_colors: true, 210 | without_details: false 211 | } 212 | ) 213 | } 214 | 215 | #[test] 216 | fn should_parse_format_json() { 217 | let format: Result = "json".parse(); 218 | assert!(format.is_ok()); 219 | assert_eq!( 220 | format.unwrap(), 221 | Format::Json { 222 | without_details: false 223 | } 224 | ) 225 | } 226 | 227 | #[test] 228 | fn should_parse_format_json_ignore_case() { 229 | let format: Result = "JsOn".parse(); 230 | assert!(format.is_ok()); 231 | assert_eq!( 232 | format.unwrap(), 233 | Format::Json { 234 | without_details: false 235 | } 236 | ) 237 | } 238 | 239 | #[test] 240 | fn should_parse_format_compact() { 241 | let format: Result = "compact".parse(); 242 | assert!(format.is_ok()); 243 | assert_eq!( 244 | format.unwrap(), 245 | Format::Compact { 246 | with_colors: true, 247 | without_details: false 248 | } 249 | ) 250 | } 251 | 252 | #[test] 253 | fn should_parse_format_compact_ignore_case() { 254 | let format: Result = "CoMpAcT".parse(); 255 | assert!(format.is_ok()); 256 | assert_eq!( 257 | format.unwrap(), 258 | Format::Compact { 259 | with_colors: true, 260 | without_details: false 261 | } 262 | ) 263 | } 264 | 265 | #[test] 266 | fn should_not_parse_format() { 267 | let format: Result = "foo".parse(); 268 | assert!(format.is_err()); 269 | assert_eq!( 270 | format.err().unwrap(), 271 | Error::new(ErrorDetail::InvalidFormat) 272 | ) 273 | } 274 | 275 | #[test] 276 | fn should_create_config_from_args() { 277 | let matches = parse_args(&[String::from("pkghist")]); 278 | let config = Config::from_arg_matches(&matches); 279 | assert_eq!(config.logfile, "/var/log/pacman.log"); 280 | assert_eq!(config.filters.is_empty(), true); 281 | assert_eq!(config.exclude, false); 282 | assert_eq!(config.with_removed, false); 283 | assert_eq!(config.removed_only, false); 284 | assert_eq!( 285 | config.format, 286 | Format::Plain { 287 | with_colors: true, 288 | without_details: false 289 | } 290 | ) 291 | } 292 | 293 | #[test] 294 | fn should_create_config_from_args_exclude() { 295 | let matches = parse_args(&[ 296 | String::from("pkghist"), 297 | String::from("--exclude"), 298 | String::from("^lib"), 299 | ]); 300 | let config = Config::from_arg_matches(&matches); 301 | assert_eq!(config.filters.is_empty(), false); 302 | assert_eq!(config.exclude, true) 303 | } 304 | 305 | #[test] 306 | fn should_create_config_from_args_no_colors() { 307 | let matches = parse_args(&[String::from("pkghist"), String::from("--no-colors")]); 308 | let config = Config::from_arg_matches(&matches); 309 | println!("{:?}", config); 310 | assert_eq!(config.logfile, "/var/log/pacman.log"); 311 | assert_eq!(config.filters.is_empty(), true); 312 | assert_eq!(config.with_removed, false); 313 | assert_eq!(config.removed_only, false); 314 | assert_eq!( 315 | config.format, 316 | Format::Plain { 317 | with_colors: false, 318 | without_details: false 319 | } 320 | ) 321 | } 322 | 323 | #[test] 324 | fn should_create_config_from_args_removed_only() { 325 | let matches = parse_args(&[String::from("pkghist"), String::from("--removed-only")]); 326 | let config = Config::from_arg_matches(&matches); 327 | assert_eq!(config.logfile, "/var/log/pacman.log"); 328 | assert_eq!(config.filters.is_empty(), true); 329 | assert_eq!(config.with_removed, false); 330 | assert_eq!(config.removed_only, true) 331 | } 332 | 333 | #[test] 334 | fn should_create_config_from_args_with_removed() { 335 | let matches = parse_args(&[String::from("pkghist"), String::from("--with-removed")]); 336 | let config = Config::from_arg_matches(&matches); 337 | assert_eq!(config.filters.is_empty(), true); 338 | assert_eq!(config.with_removed, true); 339 | assert_eq!(config.removed_only, false) 340 | } 341 | 342 | #[test] 343 | fn should_create_config_from_args_filters() { 344 | let matches = parse_args(&[String::from("pkghist"), String::from("linux")]); 345 | let config = Config::from_arg_matches(&matches); 346 | assert_eq!(config.filters.is_empty(), false); 347 | assert_eq!(config.filters.len(), 1); 348 | } 349 | 350 | #[test] 351 | fn should_create_config_from_args_format_json() { 352 | let matches = parse_args(&[ 353 | String::from("pkghist"), 354 | String::from("--output-format"), 355 | String::from("json"), 356 | ]); 357 | let config = Config::from_arg_matches(&matches); 358 | assert_eq!( 359 | config.format, 360 | Format::Json { 361 | without_details: false 362 | } 363 | ) 364 | } 365 | 366 | #[test] 367 | fn should_create_config_from_args_format_json_no_details() { 368 | let matches = parse_args(&[ 369 | String::from("pkghist"), 370 | String::from("--output-format"), 371 | String::from("json"), 372 | String::from("--no-details"), 373 | ]); 374 | let config = Config::from_arg_matches(&matches); 375 | assert_eq!( 376 | config.format, 377 | Format::Json { 378 | without_details: true 379 | } 380 | ) 381 | } 382 | 383 | #[test] 384 | fn should_create_config_from_args_format_compact() { 385 | let matches = parse_args(&[ 386 | String::from("pkghist"), 387 | String::from("--output-format"), 388 | String::from("compact"), 389 | ]); 390 | let config = Config::from_arg_matches(&matches); 391 | assert_eq!( 392 | config.format, 393 | Format::Compact { 394 | with_colors: true, 395 | without_details: false 396 | } 397 | ) 398 | } 399 | 400 | #[test] 401 | fn should_create_config_from_args_format_compact_no_details() { 402 | let matches = parse_args(&[ 403 | String::from("pkghist"), 404 | String::from("--output-format"), 405 | String::from("compact"), 406 | String::from("--no-details"), 407 | ]); 408 | let config = Config::from_arg_matches(&matches); 409 | assert_eq!( 410 | config.format, 411 | Format::Compact { 412 | with_colors: true, 413 | without_details: true 414 | } 415 | ) 416 | } 417 | 418 | #[test] 419 | fn should_create_config_from_args_limit_some() { 420 | let matches = parse_args(&[ 421 | String::from("pkghist"), 422 | String::from("--limit"), 423 | String::from("3"), 424 | ]); 425 | let config = Config::from_arg_matches(&matches); 426 | assert_eq!(config.limit, Some(3)) 427 | } 428 | 429 | #[test] 430 | fn should_create_config_from_args_limit_none() { 431 | let matches = parse_args(&[String::from("pkghist")]); 432 | let config = Config::from_arg_matches(&matches); 433 | assert_eq!(config.limit, None) 434 | } 435 | 436 | #[test] 437 | fn should_create_config_from_args_first_some() { 438 | let matches = parse_args(&[ 439 | String::from("pkghist"), 440 | String::from("--first"), 441 | String::from("50"), 442 | ]); 443 | let config = Config::from_arg_matches(&matches); 444 | assert_eq!(config.direction, Some(Direction::Forwards { n: 50 })) 445 | } 446 | 447 | #[test] 448 | fn should_create_config_from_args_first_none() { 449 | let matches = parse_args(&[String::from("pkghist")]); 450 | let config = Config::from_arg_matches(&matches); 451 | assert_eq!(config.direction, None) 452 | } 453 | 454 | #[test] 455 | fn should_create_config_from_args_last_some() { 456 | let matches = parse_args(&[ 457 | String::from("pkghist"), 458 | String::from("--last"), 459 | String::from("50"), 460 | ]); 461 | let config = Config::from_arg_matches(&matches); 462 | assert_eq!(config.direction, Some(Direction::Backwards { n: 50 })) 463 | } 464 | 465 | #[test] 466 | fn should_create_config_from_args_after() { 467 | let matches = parse_args(&[ 468 | String::from("pkghist"), 469 | String::from("--after"), 470 | String::from("2019-01-01 12:00"), 471 | ]); 472 | let config = Config::from_arg_matches(&matches); 473 | assert_eq!( 474 | config.after, 475 | Some(NaiveDateTime::new( 476 | NaiveDate::from_ymd_opt(2019, 1, 1).unwrap(), 477 | NaiveTime::from_hms_opt(12, 0, 0).unwrap(), 478 | )) 479 | ) 480 | } 481 | 482 | #[test] 483 | fn should_create_config_from_args_after_none() { 484 | let matches = parse_args(&[String::from("pkghist")]); 485 | let config = Config::from_arg_matches(&matches); 486 | assert_eq!(config.after, None) 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/pacman/filter.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use chrono::NaiveDateTime; 4 | 5 | use regex::Regex; 6 | 7 | use crate::opt::Config; 8 | use crate::pacman::group::Group; 9 | use crate::pacman::newest::Newest; 10 | use crate::pacman::range; 11 | use crate::pacman::PacmanEvent; 12 | 13 | pub trait Filter { 14 | type Event; 15 | 16 | fn without_installed(&self) -> HashMap<&String, Vec<&Self::Event>>; 17 | 18 | fn without_removed(&self) -> HashMap<&String, Vec<&Self::Event>>; 19 | 20 | fn filter_packages(&self, config: &Config) -> HashMap<&String, Vec<&Self::Event>>; 21 | } 22 | 23 | impl Filter for Vec { 24 | type Event = PacmanEvent; 25 | 26 | fn without_installed(&self) -> HashMap<&String, Vec<&PacmanEvent>> { 27 | let groups = self.group(); 28 | let mut without_installed = groups.clone(); 29 | for (package, mut events) in groups { 30 | let latest_event = events.newest(); 31 | if latest_event.action.is_installed() { 32 | without_installed.remove(package); 33 | } 34 | } 35 | without_installed 36 | } 37 | 38 | fn without_removed(&self) -> HashMap<&String, Vec<&PacmanEvent>> { 39 | let groups = self.group(); 40 | let mut without_removed = groups.clone(); 41 | for (package, mut events) in groups { 42 | let latest_event = events.newest(); 43 | if latest_event.action.is_removed() { 44 | without_removed.remove(package); 45 | } 46 | } 47 | without_removed 48 | } 49 | 50 | fn filter_packages(&self, config: &Config) -> HashMap<&String, Vec<&Self::Event>> { 51 | let packages = if config.removed_only { 52 | self.without_installed() 53 | } else if !config.with_removed { 54 | self.without_removed() 55 | } else { 56 | self.group() 57 | }; 58 | 59 | let mut filtered_packages = HashMap::new(); 60 | for (package, events) in packages { 61 | let filtered_events = filter_events(events.clone(), &config.after); 62 | if !filtered_events.is_empty() 63 | && (config.filters.is_empty() 64 | || matches_filter(package, config.exclude, &config.filters)) 65 | { 66 | filtered_packages.insert(package, filtered_events); 67 | } 68 | } 69 | 70 | limit_pacman_events( 71 | &range::range(&filtered_packages, &config.direction), 72 | config.limit, 73 | ) 74 | } 75 | } 76 | 77 | /* 78 | * - if exclude is false -> any filter must match 79 | * - if exclude is true -> all filters must match 80 | */ 81 | fn matches_filter(package: &str, exclude: bool, filters: &[Regex]) -> bool { 82 | if !exclude { 83 | filters.iter().any(|f| f.is_match(package)) 84 | } else { 85 | filters.iter().all(|f| !f.is_match(package)) 86 | } 87 | } 88 | 89 | fn filter_events<'a>( 90 | events: Vec<&'a PacmanEvent>, 91 | after: &Option, 92 | ) -> Vec<&'a PacmanEvent> { 93 | match after { 94 | Some(a) => events.into_iter().filter(|event| event.date > *a).collect(), 95 | None => events, 96 | } 97 | } 98 | 99 | fn limit_pacman_events<'a>( 100 | packages: &HashMap<&'a String, Vec<&'a PacmanEvent>>, 101 | limit: Option, 102 | ) -> HashMap<&'a String, Vec<&'a PacmanEvent>> { 103 | if let Some(l) = limit { 104 | let mut limited_packages = HashMap::new(); 105 | for (package, pacman_events) in packages { 106 | let limited = pacman_events.iter().by_ref().rev().take(l as usize).fold( 107 | Vec::new(), 108 | |mut current, event| { 109 | current.push(*event); 110 | current 111 | }, 112 | ); 113 | limited_packages.insert(*package, limited); 114 | } 115 | limited_packages 116 | } else { 117 | packages.clone() 118 | } 119 | } 120 | 121 | pub fn is_relevant_package(filters: &[String], package: &str) -> bool { 122 | filters.is_empty() || filters.contains(&String::from(package)) 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use std::fs; 128 | use std::fs::File; 129 | use std::io::Write; 130 | use std::path::Path; 131 | 132 | use chrono::{NaiveDate, NaiveTime}; 133 | use filepath::FilePath; 134 | 135 | use crate::pacman; 136 | 137 | use super::*; 138 | use crate::pacman::action::Action; 139 | 140 | #[test] 141 | fn should_match_package_starting_with() { 142 | let regex = Regex::new("^linux").unwrap(); 143 | let mut filters = Vec::new(); 144 | filters.push(regex); 145 | assert!(matches_filter("linux", false, &filters)); 146 | assert_eq!(matches_filter("utils-linux", false, &filters), false) 147 | } 148 | 149 | #[test] 150 | fn should_exclude_package_starting_with() { 151 | let regex = Regex::new("^linux").unwrap(); 152 | let mut filters = Vec::new(); 153 | filters.push(regex); 154 | assert_eq!(matches_filter("linux", true, &filters), false); 155 | assert_eq!(matches_filter("utils-linux", true, &filters), true) 156 | } 157 | 158 | #[test] 159 | fn should_not_filter_packages_when_no_filters_are_defined() { 160 | let file_name = uuid::Uuid::new_v4().to_string(); 161 | let mut file = File::create(&file_name).unwrap(); 162 | writeln!( 163 | file, 164 | "[2019-06-23 21:09] [ALPM] upgraded linux (5.1.12.arch1-1 -> 5.1.14.arch1-1) 165 | [2019-06-26 12:48] [ALPM] upgraded linux (5.1.14.arch1-1 -> 5.1.15.arch1-1) 166 | [2019-07-08 01:01] [ALPM] upgraded linux-firmware (20190618.acb56f2-1 -> 20190628.70e4394-1) 167 | [2019-07-08 01:01] [ALPM] upgraded linux (5.1.15.arch1-1 -> 5.1.16.arch1-1) 168 | [2019-07-11 22:08] [ALPM] upgraded linux (5.1.16.arch1-1 -> 5.2.arch2-1) 169 | [2019-07-16 21:09] [ALPM] upgraded linux (5.2.arch2-1 -> 5.2.1.arch1-1) 170 | [2019-03-03 10:02] [ALPM] installed bash (5.0.0-1) 171 | [2019-03-16 12:57] [ALPM] upgraded bash (5.0.0-1 -> 5.0.002-1) 172 | [2019-04-14 21:51] [ALPM] upgraded bash (5.0.002-1 -> 5.0.003-1) 173 | [2019-05-10 12:45] [ALPM] upgraded bash (5.0.003-1 -> 5.0.007-1)" 174 | ) 175 | .unwrap(); 176 | 177 | let mut config = Config::new(); 178 | config.logfile = file_name.clone(); 179 | 180 | let pacman_events = pacman::from_file(Path::new(&file_name)) 181 | .unwrap_or_else(|_| panic!("Unable to open {}", &file_name)); 182 | 183 | let groups = pacman_events.filter_packages(&config); 184 | assert_eq!(groups.keys().len(), 3); 185 | fs::remove_file(file.path().unwrap()).unwrap() 186 | } 187 | 188 | #[test] 189 | fn should_filter_packages_when_not_matching_filters() { 190 | let file_name = uuid::Uuid::new_v4().to_string(); 191 | let mut file = File::create(&file_name).unwrap(); 192 | writeln!( 193 | file, 194 | "[2019-06-23 21:09] [ALPM] upgraded linux (5.1.12.arch1-1 -> 5.1.14.arch1-1) 195 | [2019-06-26 12:48] [ALPM] upgraded linux (5.1.14.arch1-1 -> 5.1.15.arch1-1) 196 | [2019-07-08 01:01] [ALPM] upgraded linux-firmware (20190618.acb56f2-1 -> 20190628.70e4394-1) 197 | [2019-07-08 01:01] [ALPM] upgraded linux (5.1.15.arch1-1 -> 5.1.16.arch1-1) 198 | [2019-07-11 22:08] [ALPM] upgraded linux (5.1.16.arch1-1 -> 5.2.arch2-1) 199 | [2019-07-16 21:09] [ALPM] upgraded linux (5.2.arch2-1 -> 5.2.1.arch1-1) 200 | [2019-03-03 10:02] [ALPM] installed bash (5.0.0-1) 201 | [2019-03-16 12:57] [ALPM] upgraded bash (5.0.0-1 -> 5.0.002-1) 202 | [2019-04-14 21:51] [ALPM] upgraded bash (5.0.002-1 -> 5.0.003-1) 203 | [2019-05-10 12:45] [ALPM] upgraded bash (5.0.003-1 -> 5.0.007-1)" 204 | ) 205 | .unwrap(); 206 | 207 | let mut filters: Vec = Vec::new(); 208 | filters.push(Regex::new("^bash$").unwrap()); 209 | filters.push(Regex::new("^linux$").unwrap()); 210 | 211 | let mut config = Config::new(); 212 | config.logfile = file_name.clone(); 213 | config.filters = filters; 214 | 215 | let pacman_events = pacman::from_file(Path::new(&file_name)) 216 | .unwrap_or_else(|_| panic!("Unable to open {}", &file_name)); 217 | 218 | let groups = pacman_events.filter_packages(&config); 219 | assert_eq!(groups.keys().len(), 2); 220 | fs::remove_file(file.path().unwrap()).unwrap() 221 | } 222 | 223 | #[test] 224 | fn should_filter_packages_when_exclude_matches_filters() { 225 | let file_name = uuid::Uuid::new_v4().to_string(); 226 | let mut file = File::create(&file_name).unwrap(); 227 | writeln!( 228 | file, 229 | "[2019-06-23 21:09] [ALPM] upgraded linux (5.1.12.arch1-1 -> 5.1.14.arch1-1) 230 | [2019-06-26 12:48] [ALPM] upgraded linux (5.1.14.arch1-1 -> 5.1.15.arch1-1) 231 | [2019-07-08 01:01] [ALPM] upgraded linux-firmware (20190618.acb56f2-1 -> 20190628.70e4394-1) 232 | [2019-07-08 01:01] [ALPM] upgraded linux (5.1.15.arch1-1 -> 5.1.16.arch1-1) 233 | [2019-07-11 22:08] [ALPM] upgraded linux (5.1.16.arch1-1 -> 5.2.arch2-1) 234 | [2019-07-16 21:09] [ALPM] upgraded linux (5.2.arch2-1 -> 5.2.1.arch1-1) 235 | [2019-03-03 10:02] [ALPM] installed bash (5.0.0-1) 236 | [2019-03-16 12:57] [ALPM] upgraded bash (5.0.0-1 -> 5.0.002-1) 237 | [2019-04-14 21:51] [ALPM] upgraded bash (5.0.002-1 -> 5.0.003-1) 238 | [2019-05-10 12:45] [ALPM] upgraded bash (5.0.003-1 -> 5.0.007-1)" 239 | ) 240 | .unwrap(); 241 | 242 | let mut filters: Vec = Vec::new(); 243 | filters.push(Regex::new("-firmware$").unwrap()); 244 | filters.push(Regex::new("^b").unwrap()); 245 | 246 | let mut config = Config::new(); 247 | config.logfile = file_name.clone(); 248 | config.exclude = true; 249 | config.filters = filters; 250 | 251 | let pacman_events = pacman::from_file(Path::new(&file_name)) 252 | .unwrap_or_else(|_| panic!("Unable to open {}", &file_name)); 253 | 254 | let groups = pacman_events.filter_packages(&config); 255 | assert_eq!(groups.keys().len(), 1); 256 | assert_eq!(groups.contains_key(&String::from("linux")), true); 257 | fs::remove_file(file.path().unwrap()).unwrap() 258 | } 259 | 260 | #[test] 261 | fn should_filter_installed() { 262 | let file_name = uuid::Uuid::new_v4().to_string(); 263 | let mut file = File::create(&file_name).unwrap(); 264 | writeln!( 265 | file, 266 | "[2019-06-23 21:09] [ALPM] upgraded linux (5.1.12.arch1-1 -> 5.1.14.arch1-1) 267 | [2019-06-26 12:48] [ALPM] upgraded linux (5.1.14.arch1-1 -> 5.1.15.arch1-1) 268 | [2019-07-08 01:01] [ALPM] upgraded linux-firmware (20190618.acb56f2-1 -> 20190628.70e4394-1) 269 | [2019-07-08 01:01] [ALPM] upgraded linux (5.1.15.arch1-1 -> 5.1.16.arch1-1) 270 | [2019-07-11 22:08] [ALPM] upgraded linux (5.1.16.arch1-1 -> 5.2.arch2-1) 271 | [2019-07-16 21:09] [ALPM] upgraded linux (5.2.arch2-1 -> 5.2.1.arch1-1) 272 | [2019-03-03 10:02] [ALPM] installed bash (5.0.0-1) 273 | [2019-03-16 12:57] [ALPM] upgraded bash (5.0.0-1 -> 5.0.002-1) 274 | [2019-04-14 21:51] [ALPM] upgraded bash (5.0.002-1 -> 5.0.003-1) 275 | [2019-05-10 12:45] [ALPM] removed bash (5.0.003-1)" 276 | ) 277 | .unwrap(); 278 | 279 | let mut filters: Vec = Vec::new(); 280 | filters.push(Regex::new("bash").unwrap()); 281 | filters.push(Regex::new("linux").unwrap()); 282 | 283 | let mut config = Config::new(); 284 | config.logfile = file_name.clone(); 285 | config.filters = filters; 286 | config.removed_only = true; 287 | 288 | let pacman_events = pacman::from_file(Path::new(&file_name)) 289 | .unwrap_or_else(|_| panic!("Unable to open {}", &file_name)); 290 | 291 | let groups = pacman_events.filter_packages(&config); 292 | assert_eq!(groups.keys().len(), 1); 293 | fs::remove_file(file.path().unwrap()).unwrap() 294 | } 295 | 296 | #[test] 297 | fn should_keep_removed_and_installed() { 298 | let file_name = uuid::Uuid::new_v4().to_string(); 299 | let mut file = File::create(&file_name).unwrap(); 300 | writeln!( 301 | file, 302 | "[2019-06-23 21:09] [ALPM] upgraded linux (5.1.12.arch1-1 -> 5.1.14.arch1-1) 303 | [2019-06-26 12:48] [ALPM] upgraded linux (5.1.14.arch1-1 -> 5.1.15.arch1-1) 304 | [2019-07-08 01:01] [ALPM] upgraded linux-firmware (20190618.acb56f2-1 -> 20190628.70e4394-1) 305 | [2019-07-08 01:01] [ALPM] upgraded linux (5.1.15.arch1-1 -> 5.1.16.arch1-1) 306 | [2019-07-11 22:08] [ALPM] upgraded linux (5.1.16.arch1-1 -> 5.2.arch2-1) 307 | [2019-07-16 21:09] [ALPM] upgraded linux (5.2.arch2-1 -> 5.2.1.arch1-1) 308 | [2019-03-03 10:02] [ALPM] installed bash (5.0.0-1) 309 | [2019-03-16 12:57] [ALPM] upgraded bash (5.0.0-1 -> 5.0.002-1) 310 | [2019-04-14 21:51] [ALPM] upgraded bash (5.0.002-1 -> 5.0.003-1) 311 | [2019-05-10 12:45] [ALPM] removed bash (5.0.003-1)" 312 | ) 313 | .unwrap(); 314 | 315 | let mut config = Config::new(); 316 | config.logfile = file_name.clone(); 317 | config.filters = Vec::new(); 318 | config.with_removed = true; 319 | 320 | let pacman_events = pacman::from_file(Path::new(&file_name)) 321 | .unwrap_or_else(|_| panic!("Unable to open {}", &file_name)); 322 | 323 | let groups = pacman_events.filter_packages(&config); 324 | println!("{:?}", groups); 325 | assert_eq!(groups.keys().len(), 3); 326 | fs::remove_file(file.path().unwrap()).unwrap() 327 | } 328 | 329 | fn some_pacman_events() -> Vec { 330 | let mut pacman_events = Vec::new(); 331 | pacman_events.push(PacmanEvent::new( 332 | NaiveDateTime::new( 333 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 334 | NaiveTime::from_hms_opt(11, 30, 0).unwrap(), 335 | ), 336 | Action::Installed, 337 | String::from("some-package"), 338 | String::from("0.0.1"), 339 | None, 340 | )); 341 | pacman_events.push(PacmanEvent::new( 342 | NaiveDateTime::new( 343 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 344 | NaiveTime::from_hms_opt(11, 30, 0).unwrap(), 345 | ), 346 | Action::Installed, 347 | String::from("another-package"), 348 | String::from("0.0.2"), 349 | None, 350 | )); 351 | pacman_events.push(PacmanEvent::new( 352 | NaiveDateTime::new( 353 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 354 | NaiveTime::from_hms_opt(12, 30, 0).unwrap(), 355 | ), 356 | Action::Installed, 357 | String::from("another-package"), 358 | String::from("0.0.2"), 359 | Some(String::from("0.0.3")), 360 | )); 361 | pacman_events.push(PacmanEvent::new( 362 | NaiveDateTime::new( 363 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 364 | NaiveTime::from_hms_opt(12, 31, 0).unwrap(), 365 | ), 366 | Action::Removed, 367 | String::from("another-package"), 368 | String::from("0.0.2"), 369 | None, 370 | )); 371 | pacman_events.push(PacmanEvent::new( 372 | NaiveDateTime::new( 373 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 374 | NaiveTime::from_hms_opt(12, 35, 0).unwrap(), 375 | ), 376 | Action::Installed, 377 | String::from("another-package"), 378 | String::from("0.0.2"), 379 | None, 380 | )); 381 | pacman_events.push(PacmanEvent::new( 382 | NaiveDateTime::new( 383 | NaiveDate::from_ymd_opt(2019, 08, 30).unwrap(), 384 | NaiveTime::from_hms_opt(12, 35, 0).unwrap(), 385 | ), 386 | Action::Removed, 387 | String::from("no-longer-used"), 388 | String::from("0.0.2"), 389 | None, 390 | )); 391 | pacman_events 392 | } 393 | 394 | #[test] 395 | fn should_remove_installed() { 396 | // given 397 | let pacman_events = some_pacman_events(); 398 | 399 | // when 400 | let without_installed = pacman_events.without_installed(); 401 | 402 | // then 403 | assert_eq!( 404 | without_installed.contains_key(&String::from("some-package")), 405 | false 406 | ); 407 | assert_eq!( 408 | without_installed.contains_key(&String::from("another-package")), 409 | false 410 | ); 411 | assert_eq!( 412 | without_installed.contains_key(&String::from("no-longer-used")), 413 | true 414 | ) 415 | } 416 | 417 | #[test] 418 | fn should_keep_installed() { 419 | // given 420 | let pacman_events = some_pacman_events(); 421 | 422 | // when 423 | let without_removed = pacman_events.without_removed(); 424 | 425 | // then 426 | assert_eq!( 427 | without_removed.contains_key(&String::from("some-package")), 428 | true 429 | ); 430 | assert_eq!( 431 | without_removed.contains_key(&String::from("another-package")), 432 | true 433 | ); 434 | assert_eq!( 435 | without_removed.contains_key(&String::from("no-longer-used")), 436 | false 437 | ) 438 | } 439 | 440 | #[test] 441 | fn should_be_relevant_when_filters_are_empty() { 442 | let filters = Vec::new(); 443 | assert_eq!(is_relevant_package(&filters, "linux"), true) 444 | } 445 | 446 | #[test] 447 | fn should_not_be_relevant_with_filters() { 448 | let mut filters: Vec = Vec::new(); 449 | filters.push(String::from("vim")); 450 | assert_eq!(is_relevant_package(&filters, "linux"), false) 451 | } 452 | 453 | #[test] 454 | fn should_limit_pacman_events() { 455 | // given 456 | let pacman_events = some_pacman_events(); 457 | let mut group = pacman_events.without_removed(); 458 | 459 | // when 460 | let limited = limit_pacman_events(&mut group, Some(1)); 461 | 462 | // then 463 | for (_, l) in limited { 464 | assert_eq!(l.len(), 1) 465 | } 466 | } 467 | 468 | #[test] 469 | fn should_filter_events_before_date() { 470 | let pacman_events = some_pacman_events(); 471 | let refs = pacman_events.iter().collect(); 472 | let filtered = filter_events( 473 | refs, 474 | &Some(NaiveDateTime::new( 475 | NaiveDate::from_ymd_opt(2019, 8, 30).unwrap(), 476 | NaiveTime::from_hms_opt(12, 31, 0).unwrap(), 477 | )), 478 | ); 479 | 480 | assert_eq!(filtered.len(), 2) 481 | } 482 | 483 | #[test] 484 | fn should_filter_no_events() { 485 | let pacman_events = some_pacman_events(); 486 | let refs = pacman_events.iter().collect(); 487 | let filtered = filter_events(refs, &None); 488 | 489 | assert_eq!(filtered.len(), 6) 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/pkghist/format.rs: -------------------------------------------------------------------------------- 1 | use serde_json; 2 | 3 | use crate::error::Error; 4 | use crate::opt::Format; 5 | use crate::pacman::action::Action; 6 | use crate::pkghist::{Event, PackageHistory}; 7 | use termion::color; 8 | 9 | fn format_json( 10 | stdout: &mut W, 11 | packages_with_version: &[PackageHistory], 12 | without_details: bool, 13 | ) -> Result<(), Error> { 14 | let json = if without_details { 15 | let packages: Vec = packages_with_version.iter().map(|p| p.p.clone()).collect(); 16 | serde_json::to_string_pretty(&packages).unwrap() 17 | } else { 18 | serde_json::to_string_pretty(packages_with_version).unwrap() 19 | }; 20 | writeln!(stdout, "{}", json)?; 21 | Ok(()) 22 | } 23 | 24 | fn format_plain( 25 | stdout: &mut W, 26 | package_histories: &[PackageHistory], 27 | with_colors: bool, 28 | without_details: bool, 29 | ) -> Result<(), Error> { 30 | for package_history in package_histories { 31 | if with_colors { 32 | // check if last event is a removal 33 | match last_action(&package_history) { 34 | Action::Removed => write!(stdout, "{red}", red = color::Fg(color::Red))?, 35 | _ => write!(stdout, "{green}", green = color::Fg(color::Green))?, 36 | } 37 | writeln!( 38 | stdout, 39 | "{package}{reset}", 40 | package = package_history.p, 41 | reset = color::Fg(color::Reset) 42 | )? 43 | } else { 44 | writeln!(stdout, "{}", package_history.p)? 45 | } 46 | if !without_details { 47 | for event in &package_history.e { 48 | if with_colors { 49 | match event.a.parse().unwrap() { 50 | Action::Removed => write!(stdout, "{red}", red = color::Fg(color::Red))?, 51 | Action::Downgraded => { 52 | write!(stdout, "{yellow}", yellow = color::Fg(color::Yellow))? 53 | } 54 | _ => {} 55 | } 56 | writeln!( 57 | stdout, 58 | " [{date}] {action}", 59 | date = event.d, 60 | action = event.a, 61 | )?; 62 | writeln!( 63 | stdout, 64 | " {version}{reset}", 65 | version = event.v, 66 | reset = color::Fg(color::Reset) 67 | )? 68 | } else { 69 | writeln!( 70 | stdout, 71 | " [{date}] {action}", 72 | date = event.d, 73 | action = event.a 74 | )?; 75 | writeln!(stdout, " {version}", version = event.v)? 76 | } 77 | } 78 | } 79 | } 80 | Ok(()) 81 | } 82 | 83 | fn format_compact( 84 | stdout: &mut W, 85 | package_histories: &[PackageHistory], 86 | with_colors: bool, 87 | without_details: bool, 88 | ) -> Result<(), Error> { 89 | let (p_max, d_max, a_max, v_max) = max_lens(&package_histories); 90 | for package_history in package_histories { 91 | for event in &package_history.e { 92 | match (with_colors, without_details) { 93 | (with_colors, true) => { 94 | if with_colors { 95 | match event.a.parse().unwrap() { 96 | Action::Removed => { 97 | write!(stdout, "{red}", red = color::Fg(color::Red))? 98 | } 99 | Action::Downgraded => { 100 | write!(stdout, "{yellow}", yellow = color::Fg(color::Yellow))? 101 | } 102 | _ => write!(stdout, "{green}", green = color::Fg(color::Green))?, 103 | } 104 | } 105 | write!( 106 | stdout, 107 | "|{package: { 118 | if with_colors { 119 | match event.a.parse().unwrap() { 120 | Action::Removed => { 121 | write!(stdout, "{red}", red = color::Fg(color::Red))? 122 | } 123 | Action::Downgraded => { 124 | write!(stdout, "{yellow}", yellow = color::Fg(color::Yellow))? 125 | } 126 | _ => write!(stdout, "{green}", green = color::Fg(color::Green))?, 127 | } 128 | } 129 | write!( 130 | stdout, 131 | "|{package: (usize, usize, usize, usize) { 154 | let p_max = package_histories.iter().map(|p| p.p.len()).max().unwrap(); 155 | let events: Vec = package_histories.iter().flat_map(|p| p.e.clone()).collect(); 156 | let d_max = events.iter().map(|e| e.d.len()).max().unwrap(); 157 | let a_max = events.iter().map(|e| e.a.len()).max().unwrap(); 158 | let v_max = events.iter().map(|e| e.v.len()).max().unwrap(); 159 | (p_max, d_max, a_max, v_max) 160 | } 161 | 162 | fn last_action(package_history: &PackageHistory) -> Action { 163 | let last_event = package_history.e.last().unwrap(); 164 | last_event.a.parse().unwrap() 165 | } 166 | 167 | pub trait Printer { 168 | fn print( 169 | &self, 170 | stdout: &mut W, 171 | package_histories: &[PackageHistory], 172 | ) -> Result<(), Error>; 173 | } 174 | 175 | impl Printer for Format { 176 | fn print( 177 | &self, 178 | stdout: &mut W, 179 | package_histories: &[PackageHistory], 180 | ) -> Result<(), Error> { 181 | match *self { 182 | Format::Plain { 183 | with_colors, 184 | without_details, 185 | } => format_plain(stdout, package_histories, with_colors, without_details), 186 | Format::Json { without_details } => { 187 | format_json(stdout, package_histories, without_details) 188 | } 189 | Format::Compact { 190 | with_colors, 191 | without_details, 192 | } => format_compact(stdout, package_histories, with_colors, without_details), 193 | } 194 | } 195 | } 196 | 197 | #[cfg(test)] 198 | mod tests { 199 | use super::*; 200 | use Printer; 201 | 202 | #[test] 203 | fn should_print_json_to_stdout() { 204 | let package_histories = vec![PackageHistory { 205 | p: String::from("foo"), 206 | e: vec![Event { 207 | a: String::from("Installed"), 208 | v: String::from("0.0.1"), 209 | d: String::from("2019-08-26 12:00:00"), 210 | }], 211 | }]; 212 | let mut stdout = Vec::new(); 213 | Format::Json { 214 | without_details: false, 215 | } 216 | .print(&mut stdout, &package_histories) 217 | .unwrap(); 218 | let str = String::from_utf8(stdout).unwrap(); 219 | assert_eq!( 220 | str, 221 | "[\n {\n \"p\": \"foo\",\n \"e\": [\n {\n \"v\": \"0.0.1\",\n \"d\": \"2019-08-26 12:00:00\",\n \"a\": \"Installed\"\n }\n ]\n }\n]\n" 222 | ) 223 | } 224 | 225 | #[test] 226 | fn should_print_json_to_stdout_no_details() { 227 | let package_histories = vec![PackageHistory { 228 | p: String::from("foo"), 229 | e: vec![Event { 230 | a: String::from("Installed"), 231 | v: String::from("0.0.1"), 232 | d: String::from("2019-08-26 12:00:00"), 233 | }], 234 | }]; 235 | let mut stdout = Vec::new(); 236 | Format::Json { 237 | without_details: true, 238 | } 239 | .print(&mut stdout, &package_histories) 240 | .unwrap(); 241 | let str = String::from_utf8(stdout).unwrap(); 242 | assert_eq!(str, "[\n \"foo\"\n]\n") 243 | } 244 | 245 | #[test] 246 | fn should_print_to_stdout_colored() { 247 | let package_histories = vec![PackageHistory { 248 | p: String::from("foo"), 249 | e: vec![ 250 | Event { 251 | a: String::from("Upgraded"), 252 | v: String::from("0.0.2"), 253 | d: String::from("2019-08-26 12:00:00"), 254 | }, 255 | Event { 256 | a: String::from("Downgraded"), 257 | v: String::from("0.0.1"), 258 | d: String::from("2019-08-26 13:00:00"), 259 | }, 260 | Event { 261 | a: String::from("Removed"), 262 | v: String::from("0.0.1"), 263 | d: String::from("2019-08-26 14:00:00"), 264 | }, 265 | ], 266 | }]; 267 | let mut stdout = Vec::new(); 268 | Format::Plain { 269 | with_colors: true, 270 | without_details: false, 271 | } 272 | .print(&mut stdout, &package_histories) 273 | .unwrap(); 274 | let str = String::from_utf8(stdout).unwrap(); 275 | assert_eq!( 276 | str, 277 | "\u{1b}[38;5;1mfoo\u{1b}[39m\n [2019-08-26 12:00:00] Upgraded\n 0.0.2\u{1b}[39m\n\u{1b}[38;5;3m [2019-08-26 13:00:00] Downgraded\n 0.0.1\u{1b}[39m\n\u{1b}[38;5;1m [2019-08-26 14:00:00] Removed\n 0.0.1\u{1b}[39m\n" 278 | ) 279 | } 280 | 281 | #[test] 282 | fn should_print_to_stdout_colored_no_details() { 283 | let package_histories = vec![PackageHistory { 284 | p: String::from("foo"), 285 | e: vec![Event { 286 | a: String::from("Installed"), 287 | v: String::from("0.0.1"), 288 | d: String::from("2019-08-26 12:00:00"), 289 | }], 290 | }]; 291 | let mut stdout = Vec::new(); 292 | Format::Plain { 293 | with_colors: true, 294 | without_details: true, 295 | } 296 | .print(&mut stdout, &package_histories) 297 | .unwrap(); 298 | let str = String::from_utf8(stdout).unwrap(); 299 | assert_eq!(str, "\u{1b}[38;5;2mfoo\u{1b}[39m\n") 300 | } 301 | 302 | #[test] 303 | fn should_print_to_stdout_no_colors() { 304 | let package_histories = vec![PackageHistory { 305 | p: String::from("foo"), 306 | e: vec![Event { 307 | a: String::from("Installed"), 308 | v: String::from("0.0.1"), 309 | d: String::from("2019-08-26 12:00:00"), 310 | }], 311 | }]; 312 | let mut stdout = Vec::new(); 313 | Format::Plain { 314 | with_colors: false, 315 | without_details: false, 316 | } 317 | .print(&mut stdout, &package_histories) 318 | .unwrap(); 319 | let str = String::from_utf8(stdout).unwrap(); 320 | assert_eq!(str, "foo\n [2019-08-26 12:00:00] Installed\n 0.0.1\n") 321 | } 322 | 323 | #[test] 324 | fn should_print_to_stdout_no_colors_no_details() { 325 | let package_histories = vec![PackageHistory { 326 | p: String::from("foo"), 327 | e: vec![Event { 328 | a: String::from("Installed"), 329 | v: String::from("0.0.1"), 330 | d: String::from("2019-08-26 12:00:00"), 331 | }], 332 | }]; 333 | let mut stdout = Vec::new(); 334 | Format::Plain { 335 | with_colors: false, 336 | without_details: true, 337 | } 338 | .print(&mut stdout, &package_histories) 339 | .unwrap(); 340 | let str = String::from_utf8(stdout).unwrap(); 341 | assert_eq!(str, "foo\n") 342 | } 343 | 344 | #[test] 345 | fn should_print_compact_to_stdout() { 346 | let package_histories = vec![PackageHistory { 347 | p: String::from("foo"), 348 | e: vec![ 349 | Event { 350 | a: String::from("Upgraded"), 351 | v: String::from("0.0.2"), 352 | d: String::from("2019-08-26 12:00:00"), 353 | }, 354 | Event { 355 | a: String::from("Downgraded"), 356 | v: String::from("0.0.1"), 357 | d: String::from("2019-08-26 13:00:00"), 358 | }, 359 | Event { 360 | a: String::from("Removed"), 361 | v: String::from("0.0.1"), 362 | d: String::from("2019-08-26 14:00:00"), 363 | }, 364 | ], 365 | }]; 366 | let mut stdout = Vec::new(); 367 | Format::Compact { 368 | with_colors: true, 369 | without_details: false, 370 | } 371 | .print(&mut stdout, &package_histories) 372 | .unwrap(); 373 | let str = String::from_utf8(stdout).unwrap(); 374 | assert_eq!( 375 | str, 376 | "\u{1b}[38;5;2m|foo|2019-08-26 12:00:00|Upgraded |0.0.2|\u{1b}[39m\n\ 377 | \u{1b}[38;5;3m|foo|2019-08-26 13:00:00|Downgraded|0.0.1|\u{1b}[39m\n\ 378 | \u{1b}[38;5;1m|foo|2019-08-26 14:00:00|Removed |0.0.1|\u{1b}[39m\n" 379 | ) 380 | } 381 | 382 | #[test] 383 | fn should_print_compact_to_stdout_no_colors() { 384 | let package_histories = vec![PackageHistory { 385 | p: String::from("foo"), 386 | e: vec![Event { 387 | a: String::from("Upgraded"), 388 | v: String::from("0.0.2"), 389 | d: String::from("2019-08-26 12:00:00"), 390 | }], 391 | }]; 392 | let mut stdout = Vec::new(); 393 | Format::Compact { 394 | with_colors: false, 395 | without_details: false, 396 | } 397 | .print(&mut stdout, &package_histories) 398 | .unwrap(); 399 | let str = String::from_utf8(stdout).unwrap(); 400 | assert_eq!(str, "|foo|2019-08-26 12:00:00|Upgraded|0.0.2|\n") 401 | } 402 | 403 | #[test] 404 | fn should_print_compact_to_stdout_no_details() { 405 | let package_histories = vec![PackageHistory { 406 | p: String::from("foo"), 407 | e: vec![ 408 | Event { 409 | a: String::from("Installed"), 410 | v: String::from("0.0.2"), 411 | d: String::from("2019-08-26 12:00:00"), 412 | }, 413 | Event { 414 | a: String::from("Downgraded"), 415 | v: String::from("0.0.1"), 416 | d: String::from("2019-08-26 13:00:00"), 417 | }, 418 | Event { 419 | a: String::from("Removed"), 420 | v: String::from("0.0.1"), 421 | d: String::from("2019-08-26 14:00:00"), 422 | }, 423 | ], 424 | }]; 425 | let mut stdout = Vec::new(); 426 | Format::Compact { 427 | with_colors: true, 428 | without_details: true, 429 | } 430 | .print(&mut stdout, &package_histories) 431 | .unwrap(); 432 | let str = String::from_utf8(stdout).unwrap(); 433 | assert_eq!( 434 | str, 435 | "\u{1b}[38;5;2m|foo|\u{1b}[39m\n\ 436 | \u{1b}[38;5;3m|foo|\u{1b}[39m\n\ 437 | \u{1b}[38;5;1m|foo|\u{1b}[39m\n" 438 | ) 439 | } 440 | 441 | #[test] 442 | fn should_print_compact_to_stdout_no_details_no_colors() { 443 | let package_histories = vec![PackageHistory { 444 | p: String::from("foo"), 445 | e: vec![Event { 446 | a: String::from("Installed"), 447 | v: String::from("0.0.1"), 448 | d: String::from("2019-08-26 12:00:00"), 449 | }], 450 | }]; 451 | let mut stdout = Vec::new(); 452 | Format::Compact { 453 | with_colors: false, 454 | without_details: true, 455 | } 456 | .print(&mut stdout, &package_histories) 457 | .unwrap(); 458 | let str = String::from_utf8(stdout).unwrap(); 459 | assert_eq!(str, "|foo|\n") 460 | } 461 | 462 | #[test] 463 | fn should_get_max_lens() { 464 | let package_histories = vec![ 465 | PackageHistory { 466 | p: String::from("foo"), 467 | e: vec![ 468 | Event { 469 | a: String::from("Installed"), 470 | v: String::from("0.0.1"), 471 | d: String::from("2019-08-26 12:00:00"), 472 | }, 473 | Event { 474 | a: String::from("Upgraded"), 475 | v: String::from("0.0.2"), 476 | d: String::from("2019-08-30 13:30:00"), 477 | }, 478 | ], 479 | }, 480 | PackageHistory { 481 | p: String::from("another"), 482 | e: vec![ 483 | Event { 484 | a: String::from("Installed"), 485 | v: String::from("1.0.1"), 486 | d: String::from("2019-08-27 12:00:00"), 487 | }, 488 | Event { 489 | a: String::from("Upgraded"), 490 | v: String::from("1.0.2-deadbeef"), 491 | d: String::from("2019-09-01 13:30:00"), 492 | }, 493 | ], 494 | }, 495 | ]; 496 | let (p_max, d_max, a_max, v_max) = max_lens(&package_histories); 497 | assert_eq!(p_max, 7); 498 | assert_eq!(d_max, 19); 499 | assert_eq!(a_max, 9); 500 | assert_eq!(v_max, 14) 501 | } 502 | 503 | #[test] 504 | fn should_get_last_action_removed() { 505 | let package_history = PackageHistory { 506 | p: String::from("another"), 507 | e: vec![ 508 | Event { 509 | a: String::from("Installed"), 510 | v: String::from("1.0.1"), 511 | d: String::from("2019-08-27 12:00:00"), 512 | }, 513 | Event { 514 | a: String::from("Removed"), 515 | v: String::from("1.0.2-deadbeef"), 516 | d: String::from("2019-09-01 13:30:00"), 517 | }, 518 | ], 519 | }; 520 | let action = last_action(&package_history); 521 | assert_eq!(action, Action::Removed) 522 | } 523 | 524 | #[test] 525 | fn should_get_last_action_upgraded() { 526 | let package_history = PackageHistory { 527 | p: String::from("another"), 528 | e: vec![ 529 | Event { 530 | a: String::from("Installed"), 531 | v: String::from("1.0.1"), 532 | d: String::from("2019-08-27 12:00:00"), 533 | }, 534 | Event { 535 | a: String::from("Upgraded"), 536 | v: String::from("1.0.2-deadbeef"), 537 | d: String::from("2019-09-01 13:30:00"), 538 | }, 539 | ], 540 | }; 541 | let action = last_action(&package_history); 542 | assert_eq!(action, Action::Upgraded) 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android_system_properties" 16 | version = "0.1.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 19 | dependencies = [ 20 | "libc", 21 | ] 22 | 23 | [[package]] 24 | name = "anstream" 25 | version = "0.3.2" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 28 | dependencies = [ 29 | "anstyle", 30 | "anstyle-parse", 31 | "anstyle-query", 32 | "anstyle-wincon", 33 | "colorchoice", 34 | "is-terminal", 35 | "utf8parse", 36 | ] 37 | 38 | [[package]] 39 | name = "anstyle" 40 | version = "1.0.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 43 | 44 | [[package]] 45 | name = "anstyle-parse" 46 | version = "0.2.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 49 | dependencies = [ 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-query" 55 | version = "1.0.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 58 | dependencies = [ 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "anstyle-wincon" 64 | version = "1.0.1" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 67 | dependencies = [ 68 | "anstyle", 69 | "windows-sys", 70 | ] 71 | 72 | [[package]] 73 | name = "autocfg" 74 | version = "1.1.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 77 | 78 | [[package]] 79 | name = "bitflags" 80 | version = "1.3.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 83 | 84 | [[package]] 85 | name = "bumpalo" 86 | version = "3.12.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" 89 | 90 | [[package]] 91 | name = "cc" 92 | version = "1.0.79" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 95 | 96 | [[package]] 97 | name = "cfg-if" 98 | version = "1.0.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 101 | 102 | [[package]] 103 | name = "chrono" 104 | version = "0.4.24" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" 107 | dependencies = [ 108 | "iana-time-zone", 109 | "js-sys", 110 | "num-integer", 111 | "num-traits", 112 | "time", 113 | "wasm-bindgen", 114 | "winapi", 115 | ] 116 | 117 | [[package]] 118 | name = "clap" 119 | version = "4.2.7" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" 122 | dependencies = [ 123 | "clap_builder", 124 | ] 125 | 126 | [[package]] 127 | name = "clap_builder" 128 | version = "4.2.7" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" 131 | dependencies = [ 132 | "anstream", 133 | "anstyle", 134 | "bitflags", 135 | "clap_lex", 136 | "once_cell", 137 | "strsim", 138 | ] 139 | 140 | [[package]] 141 | name = "clap_complete" 142 | version = "4.2.3" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "1594fe2312ec4abf402076e407628f5c313e54c32ade058521df4ee34ecac8a8" 145 | dependencies = [ 146 | "clap", 147 | ] 148 | 149 | [[package]] 150 | name = "clap_lex" 151 | version = "0.4.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" 154 | 155 | [[package]] 156 | name = "colorchoice" 157 | version = "1.0.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 160 | 161 | [[package]] 162 | name = "core-foundation-sys" 163 | version = "0.8.4" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 166 | 167 | [[package]] 168 | name = "either" 169 | version = "1.8.1" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 172 | 173 | [[package]] 174 | name = "errno" 175 | version = "0.3.1" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 178 | dependencies = [ 179 | "errno-dragonfly", 180 | "libc", 181 | "windows-sys", 182 | ] 183 | 184 | [[package]] 185 | name = "errno-dragonfly" 186 | version = "0.1.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 189 | dependencies = [ 190 | "cc", 191 | "libc", 192 | ] 193 | 194 | [[package]] 195 | name = "filepath" 196 | version = "0.1.2" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "f7faa16fcec147281a1719947edb44af4f9124964bf7476bd5f5356a48e44dcc" 199 | dependencies = [ 200 | "libc", 201 | "winapi", 202 | ] 203 | 204 | [[package]] 205 | name = "getrandom" 206 | version = "0.2.9" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" 209 | dependencies = [ 210 | "cfg-if", 211 | "libc", 212 | "wasi 0.11.0+wasi-snapshot-preview1", 213 | ] 214 | 215 | [[package]] 216 | name = "hermit-abi" 217 | version = "0.3.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 220 | 221 | [[package]] 222 | name = "iana-time-zone" 223 | version = "0.1.56" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" 226 | dependencies = [ 227 | "android_system_properties", 228 | "core-foundation-sys", 229 | "iana-time-zone-haiku", 230 | "js-sys", 231 | "wasm-bindgen", 232 | "windows", 233 | ] 234 | 235 | [[package]] 236 | name = "iana-time-zone-haiku" 237 | version = "0.1.2" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 240 | dependencies = [ 241 | "cc", 242 | ] 243 | 244 | [[package]] 245 | name = "io-lifetimes" 246 | version = "1.0.10" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" 249 | dependencies = [ 250 | "hermit-abi", 251 | "libc", 252 | "windows-sys", 253 | ] 254 | 255 | [[package]] 256 | name = "is-terminal" 257 | version = "0.4.7" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 260 | dependencies = [ 261 | "hermit-abi", 262 | "io-lifetimes", 263 | "rustix", 264 | "windows-sys", 265 | ] 266 | 267 | [[package]] 268 | name = "itertools" 269 | version = "0.10.5" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 272 | dependencies = [ 273 | "either", 274 | ] 275 | 276 | [[package]] 277 | name = "itoa" 278 | version = "1.0.6" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 281 | 282 | [[package]] 283 | name = "js-sys" 284 | version = "0.3.62" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" 287 | dependencies = [ 288 | "wasm-bindgen", 289 | ] 290 | 291 | [[package]] 292 | name = "lazy_static" 293 | version = "1.4.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 296 | 297 | [[package]] 298 | name = "libc" 299 | version = "0.2.144" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 302 | 303 | [[package]] 304 | name = "linux-raw-sys" 305 | version = "0.3.7" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" 308 | 309 | [[package]] 310 | name = "log" 311 | version = "0.4.17" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 314 | dependencies = [ 315 | "cfg-if", 316 | ] 317 | 318 | [[package]] 319 | name = "memchr" 320 | version = "2.5.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 323 | 324 | [[package]] 325 | name = "num-integer" 326 | version = "0.1.45" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 329 | dependencies = [ 330 | "autocfg", 331 | "num-traits", 332 | ] 333 | 334 | [[package]] 335 | name = "num-traits" 336 | version = "0.2.15" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 339 | dependencies = [ 340 | "autocfg", 341 | ] 342 | 343 | [[package]] 344 | name = "numtoa" 345 | version = "0.1.0" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" 348 | 349 | [[package]] 350 | name = "once_cell" 351 | version = "1.17.1" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 354 | 355 | [[package]] 356 | name = "pkghist" 357 | version = "0.7.0" 358 | dependencies = [ 359 | "chrono", 360 | "clap", 361 | "clap_complete", 362 | "filepath", 363 | "itertools", 364 | "lazy_static", 365 | "regex", 366 | "serde", 367 | "serde_json", 368 | "termion", 369 | "uuid", 370 | ] 371 | 372 | [[package]] 373 | name = "proc-macro2" 374 | version = "1.0.56" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" 377 | dependencies = [ 378 | "unicode-ident", 379 | ] 380 | 381 | [[package]] 382 | name = "quote" 383 | version = "1.0.27" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" 386 | dependencies = [ 387 | "proc-macro2", 388 | ] 389 | 390 | [[package]] 391 | name = "redox_syscall" 392 | version = "0.2.16" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 395 | dependencies = [ 396 | "bitflags", 397 | ] 398 | 399 | [[package]] 400 | name = "redox_termios" 401 | version = "0.1.2" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" 404 | dependencies = [ 405 | "redox_syscall", 406 | ] 407 | 408 | [[package]] 409 | name = "regex" 410 | version = "1.8.1" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" 413 | dependencies = [ 414 | "aho-corasick", 415 | "memchr", 416 | "regex-syntax", 417 | ] 418 | 419 | [[package]] 420 | name = "regex-syntax" 421 | version = "0.7.1" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" 424 | 425 | [[package]] 426 | name = "rustix" 427 | version = "0.37.25" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "d4eb579851244c2c03e7c24f501c3432bed80b8f720af1d6e5b0e0f01555a035" 430 | dependencies = [ 431 | "bitflags", 432 | "errno", 433 | "io-lifetimes", 434 | "libc", 435 | "linux-raw-sys", 436 | "windows-sys", 437 | ] 438 | 439 | [[package]] 440 | name = "ryu" 441 | version = "1.0.13" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 444 | 445 | [[package]] 446 | name = "serde" 447 | version = "1.0.163" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" 450 | dependencies = [ 451 | "serde_derive", 452 | ] 453 | 454 | [[package]] 455 | name = "serde_derive" 456 | version = "1.0.163" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" 459 | dependencies = [ 460 | "proc-macro2", 461 | "quote", 462 | "syn", 463 | ] 464 | 465 | [[package]] 466 | name = "serde_json" 467 | version = "1.0.96" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" 470 | dependencies = [ 471 | "itoa", 472 | "ryu", 473 | "serde", 474 | ] 475 | 476 | [[package]] 477 | name = "strsim" 478 | version = "0.10.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 481 | 482 | [[package]] 483 | name = "syn" 484 | version = "2.0.15" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" 487 | dependencies = [ 488 | "proc-macro2", 489 | "quote", 490 | "unicode-ident", 491 | ] 492 | 493 | [[package]] 494 | name = "termion" 495 | version = "2.0.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90" 498 | dependencies = [ 499 | "libc", 500 | "numtoa", 501 | "redox_syscall", 502 | "redox_termios", 503 | ] 504 | 505 | [[package]] 506 | name = "time" 507 | version = "0.1.45" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 510 | dependencies = [ 511 | "libc", 512 | "wasi 0.10.0+wasi-snapshot-preview1", 513 | "winapi", 514 | ] 515 | 516 | [[package]] 517 | name = "unicode-ident" 518 | version = "1.0.8" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 521 | 522 | [[package]] 523 | name = "utf8parse" 524 | version = "0.2.1" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 527 | 528 | [[package]] 529 | name = "uuid" 530 | version = "0.8.2" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" 533 | dependencies = [ 534 | "getrandom", 535 | ] 536 | 537 | [[package]] 538 | name = "wasi" 539 | version = "0.10.0+wasi-snapshot-preview1" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 542 | 543 | [[package]] 544 | name = "wasi" 545 | version = "0.11.0+wasi-snapshot-preview1" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 548 | 549 | [[package]] 550 | name = "wasm-bindgen" 551 | version = "0.2.85" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" 554 | dependencies = [ 555 | "cfg-if", 556 | "wasm-bindgen-macro", 557 | ] 558 | 559 | [[package]] 560 | name = "wasm-bindgen-backend" 561 | version = "0.2.85" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" 564 | dependencies = [ 565 | "bumpalo", 566 | "log", 567 | "once_cell", 568 | "proc-macro2", 569 | "quote", 570 | "syn", 571 | "wasm-bindgen-shared", 572 | ] 573 | 574 | [[package]] 575 | name = "wasm-bindgen-macro" 576 | version = "0.2.85" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" 579 | dependencies = [ 580 | "quote", 581 | "wasm-bindgen-macro-support", 582 | ] 583 | 584 | [[package]] 585 | name = "wasm-bindgen-macro-support" 586 | version = "0.2.85" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" 589 | dependencies = [ 590 | "proc-macro2", 591 | "quote", 592 | "syn", 593 | "wasm-bindgen-backend", 594 | "wasm-bindgen-shared", 595 | ] 596 | 597 | [[package]] 598 | name = "wasm-bindgen-shared" 599 | version = "0.2.85" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" 602 | 603 | [[package]] 604 | name = "winapi" 605 | version = "0.3.9" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 608 | dependencies = [ 609 | "winapi-i686-pc-windows-gnu", 610 | "winapi-x86_64-pc-windows-gnu", 611 | ] 612 | 613 | [[package]] 614 | name = "winapi-i686-pc-windows-gnu" 615 | version = "0.4.0" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 618 | 619 | [[package]] 620 | name = "winapi-x86_64-pc-windows-gnu" 621 | version = "0.4.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 624 | 625 | [[package]] 626 | name = "windows" 627 | version = "0.48.0" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" 630 | dependencies = [ 631 | "windows-targets", 632 | ] 633 | 634 | [[package]] 635 | name = "windows-sys" 636 | version = "0.48.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 639 | dependencies = [ 640 | "windows-targets", 641 | ] 642 | 643 | [[package]] 644 | name = "windows-targets" 645 | version = "0.48.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 648 | dependencies = [ 649 | "windows_aarch64_gnullvm", 650 | "windows_aarch64_msvc", 651 | "windows_i686_gnu", 652 | "windows_i686_msvc", 653 | "windows_x86_64_gnu", 654 | "windows_x86_64_gnullvm", 655 | "windows_x86_64_msvc", 656 | ] 657 | 658 | [[package]] 659 | name = "windows_aarch64_gnullvm" 660 | version = "0.48.0" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 663 | 664 | [[package]] 665 | name = "windows_aarch64_msvc" 666 | version = "0.48.0" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 669 | 670 | [[package]] 671 | name = "windows_i686_gnu" 672 | version = "0.48.0" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 675 | 676 | [[package]] 677 | name = "windows_i686_msvc" 678 | version = "0.48.0" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 681 | 682 | [[package]] 683 | name = "windows_x86_64_gnu" 684 | version = "0.48.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 687 | 688 | [[package]] 689 | name = "windows_x86_64_gnullvm" 690 | version = "0.48.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 693 | 694 | [[package]] 695 | name = "windows_x86_64_msvc" 696 | version = "0.48.0" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 699 | --------------------------------------------------------------------------------