diff --git a/.github/workflows/cargo_clippy_lib.yml b/.github/workflows/cargo_clippy_lib.yml index 97e21e18..9ba077ec 100644 --- a/.github/workflows/cargo_clippy_lib.yml +++ b/.github/workflows/cargo_clippy_lib.yml @@ -8,11 +8,38 @@ jobs: steps: - uses: actions/checkout@v1 - run: | - rustup component add clippy + + rustup toolchain install nightly + rustup +nightly component add clippy - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features -p git_function_history + toolchain: nightly + args: -p git_function_history --features unstable + # --features c_lang + # nightly-clang-test: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - name: setup rust + # uses: dtolnay/rust-toolchain@stable + # with: + # toolchain: nightly + # override: true + # - name: test + # run: | + # cargo +nightly test -p git_function_history --features unstable --features c_lang -- --nocapture + + # test-clang: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v3 + # - name: setup rust + # uses: dtolnay/rust-toolchain@stable + # - name: test + # run: | + # cargo test -p git_function_history --features c_lang -- --nocapture + test: runs-on: ubuntu-latest steps: @@ -21,4 +48,17 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: test run: | - cargo test -p git_function_history -- --nocapture \ No newline at end of file + cargo test -p git_function_history -- --nocapture + + test-nightly: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: setup rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + override: true + - name: test + run: | + cargo +nightly test -p git_function_history --features unstable -- --nocapture \ No newline at end of file diff --git a/.github/workflows/chanelog.yaml b/.github/workflows/chanelog.yaml index bb179675..e5865d04 100644 --- a/.github/workflows/chanelog.yaml +++ b/.github/workflows/chanelog.yaml @@ -1,4 +1,4 @@ -name: Action Test +name: changelog-generator on: [push] @@ -25,6 +25,6 @@ jobs: - name: Commit the changelog uses: EndBug/add-and-commit@v7 with: - message: "chore: update changelog" + message: "update changelog" add: ${{ steps.git-cliff.outputs.changelog }} \ No newline at end of file diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml new file mode 100644 index 00000000..08ebd1dc --- /dev/null +++ b/.github/workflows/msrv.yml @@ -0,0 +1,30 @@ +name: msrv-badge + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + create-msrv-badge: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: spenserblack/actions-msrv@v0.4.1 + id: get-msrv + with: + set: true + - name: Create Badge + run: curl https://img.shields.io/badge/minimum%20rust%20version-${{ steps.get-msrv.outputs.msrv }}-blue > resources/msrv.svg + - name: Commit Badge + # If there are no changes to the badge this would error out. But it + # isn't a problem if there were no changes, so errors are allowed. + continue-on-error: true + run: | + git add resources/msrv.svg + git add **/Cargo.toml + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -m "Update MSRV badge [Skip CI]" + git push \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f61749..a59fe235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,20 +12,40 @@ All notable changes to this project will be documented in this file. - Update changelog - Update changelog - Update changelog +- Update changelog +- Update changelog +- Update changelog +- Update changelog + +### TODO + +- Figure out how to map filers and seraches based languges ### Changelog - Trying to fix changelog +### Gui + +- Added scrollbar to command builder & now supports other languages +- Started working on clickable dates/commits using list + +### Lib + +- Python works besides for one edge case when the function is the last node +- Pl filters now working very messy and boilerplatety +- Fixed bug where I didnt understand how cfg-if works, also filter_by macro works just neeeds docs + ### Tui - Saving search history to a file now +- Shortend filter loc +- Added more filters ## [2.1.0] - 2022-09-28 ### Library -- Added git filters for commit, aothor and emai, messagel - More parllesim - Trying to optimize threading realizng the problem is not with the trreading but with something else - Added parelel as optinal (but default feature diff --git a/README.md b/README.md index 8791227a..7cca97d9 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,7 @@ -# ![Custom badge](https://img.shields.io/endpoint?color=green&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmendelsshop%2Fgit_function_history%2Fstats%2Floc.json) ![Custom badge](https://img.shields.io/endpoint?color=green&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmendelsshop%2Fgit_function_history%2Fstats%2Fdownloads.json) [![dependency status](https://deps.rs/repo/github/mendelsshop/git_function_history/status.svg)](https://deps.rs/repo/github/mendelsshop/git_function_history) +# ![Custom badge](https://img.shields.io/endpoint?color=green&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmendelsshop%2Fgit_function_history%2Fstats%2Floc.json) ![Custom badge](https://img.shields.io/endpoint?color=green&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmendelsshop%2Fgit_function_history%2Fstats%2Fdownloads.json) [![dependency status](https://deps.rs/repo/github/mendelsshop/git_function_history/status.svg)](https://deps.rs/repo/github/mendelsshop/git_function_history) ![msrv](./resources/msrv.svg) # git function history -## benchmarks - -Parser (main) vs Regex approach benchmarks: -| approach| expensive| relative| date-range | -| --- | --- | --- | --- | -|regex| 313 second(s) | 22 second(s) | 8 second(s) | -|parser| 22 second(s) | 21 second(s)| 1 second(s) | - -* These benchmarks were done in debug mode on a Ryzen 7 5700u with 16Gb of ram. - ## crates in this project * [git-function-history-lib](https://github.com/mendelsshop/git_function_history/tree/main/git-function-history-lib) - the library itself diff --git a/TODO.md b/TODO.md index 54b80bb7..3559c091 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ - GUI - [x] fix `thread '' panicked at 'channel disconnected', function_history_backend_thread/src/lib.rs:33:25` error (handling when the channel is disconnected at the end of the program) - - [x] add new documentation for the new filters and fix some old documentation that talks about filter commitfunctions and files etc + - [x] add new documentation for the new filters and fix some old documentation that talks about filter and files etc - TUI - [x] use a proper input box for the edit bar, so that delete and scrolling the input works - [x] finish documentation @@ -23,5 +23,27 @@ - [ ] add more and better ways to filter dates - [x] add filters for git specific stuff like author, committer, etc - [ ] ability to get a git repo from a url using something like git clone - - [ ] add support for other languages (currently only supports rust) + - [/] add support for other languages (currently only supports rust) - [x] save search queries and filters to a file + - [ ] rework the way filters and filefilters are handled ie maybe use a builder pattern + - [/] remove all potentially panicking code + +- release 7.0: + - python: + - [x] save parent function and classes + - [x] save kwargs and varargs etc using the args enum and be able to filter by all args or just kwargs etc + - ruby: + - [ ] save kwargs and varargs etc using the args enum and be able to filter by all args or just kwargs etc + - gui: + - [ ] make the list of dates clickable so when you click on a date/commit it will automatically run a search for that date/commit + - [ ] make list command a table wiht rows and columns for date, commit, author, message, etc + - [ ] (possibly) use tree sitter to provide syntax highlighting + - tui: + - [ ] make list command a table wiht rows and columns for date, commit, author, message, etc + - [ ] (possibly) use tree sitter to provide syntax highlighting + - lib: + - [x] possibly stop using Commmad::new("git") and use https://crates.io/crates/git_rs OR https://crates.io/crates/rs-git-lib OR https://crates.io/crates/gitoxide, to imporve performance with program compibilty assistant sevice on windows + + - [ ] move language module into its own crate + - general: + - [/] update readmes with feautes and benchamrks specifically the repo & git-function-history-lib readmes diff --git a/cargo-function-history/Cargo.toml b/cargo-function-history/Cargo.toml index 4e1f3d16..5db69934 100644 --- a/cargo-function-history/Cargo.toml +++ b/cargo-function-history/Cargo.toml @@ -13,17 +13,18 @@ description = "cargo frontend for git-function-history" [features] default = ["parallel"] parallel = ["git_function_history/parallel", "function_history_backend_thread/parallel"] -not-parallel = [] +# c_lang = ["function_history_backend_thread/c_lang", "git_function_history/c_lang"] +unstable = ["function_history_backend_thread/unstable", "git_function_history/unstable"] [dependencies] -git_function_history = { path = "../git-function-history-lib", version = "0.6.2", default-features = false} -lazy_static = "1.3.0" -tui = { version = "0.19", features = ["crossterm"], default-features = false } +git_function_history = { path = "../git-function-history-lib", version = "0.7.0", default-features = false} +lazy_static = "1.4.0" +tui = { version = "0.19.0", features = ["crossterm"], default-features = false } crossterm = "0.25.0" -tokio = { version = "1.21.2", features = ["full"] } -eyre = "0.6" +tokio = { version = "1.24.1", features = ["full"] } +eyre = "0.6.8" dirs = "4.0.0" simple_file_logger = "0.3.1" -log = "0.4" -function_history_backend_thread = { path = "../function_history_backend_thread", version = "0.2.2", default-features = false} -tui-input = "0.5.1" \ No newline at end of file +log = "0.4.17" +function_history_backend_thread = { path = "../function_history_backend_thread", version = "0.3.0", default-features = false} +tui-input = "0.6.1" diff --git a/cargo-function-history/README.md b/cargo-function-history/README.md index a0911ed1..3764aaf3 100644 --- a/cargo-function-history/README.md +++ b/cargo-function-history/README.md @@ -1,4 +1,4 @@ -# [![crates.io](https://img.shields.io/crates/v/cargo-function-history.svg?label=latest%20version)](https://crates.io/crates/cargo-function-history) [![Crates.io](https://img.shields.io/crates/d/cargo-function-history?label=crates.io%20downloads)](https://crates.io/crates/cargo-function-history) +# [![crates.io](https://img.shields.io/crates/v/cargo-function-history.svg?label=latest%20version)](https://crates.io/crates/cargo-function-history) [![Crates.io](https://img.shields.io/crates/d/cargo-function-history?label=crates.io%20downloads)](https://crates.io/crates/cargo-function-history) ![msrv](../resources/msrv.svg) # cargo function history diff --git a/cargo-function-history/src/app/actions.rs b/cargo-function-history/src/app/actions.rs index 8e4614b4..2c64c96c 100644 --- a/cargo-function-history/src/app/actions.rs +++ b/cargo-function-history/src/app/actions.rs @@ -1,6 +1,8 @@ -use std::collections::HashMap; -use std::fmt::{self, Display}; -use std::slice::Iter; +use std::{ + collections::HashMap, + fmt::{self, Display}, + slice::Iter, +}; use crate::keys::Key; @@ -51,17 +53,20 @@ impl Action { /// Could display a user friendly short description of action impl Display for Action { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let str = match self { - Action::Quit => "Quit", - Action::TextEdit => "TextEdit", - Action::ScrollUp => "ScrollUp", - Action::ScrollDown => "ScrollDown", - Action::BackCommit => "BackCommit", - Action::ForwardCommit => "ForwardCommit", - Action::BackFile => "BackFile", - Action::ForwardFile => "ForwardFile", - }; - write!(f, "{}", str) + write!( + f, + "{}", + match self { + Action::Quit => "Quit", + Action::TextEdit => "TextEdit", + Action::ScrollUp => "ScrollUp", + Action::ScrollDown => "ScrollDown", + Action::BackCommit => "BackCommit", + Action::ForwardCommit => "ForwardCommit", + Action::BackFile => "BackFile", + Action::ForwardFile => "ForwardFile", + } + ) } } @@ -113,7 +118,7 @@ impl From> for Actions { .map(Action::to_string) .collect::>() .join(", "); - format!("Conflict key {} with actions {}", key, actions) + format!("Conflict key {key} with actions {actions}") }) .collect::>(); if !errors.is_empty() { diff --git a/cargo-function-history/src/app/mod.rs b/cargo-function-history/src/app/mod.rs index 7d90ccab..01f06806 100644 --- a/cargo-function-history/src/app/mod.rs +++ b/cargo-function-history/src/app/mod.rs @@ -1,11 +1,10 @@ -use self::actions::Actions; -use self::state::AppState; +use self::{actions::Actions, state::AppState}; use crate::{app::actions::Action, keys::Key}; use function_history_backend_thread::types::{ CommandResult, FilterType, FullCommand, ListType, Status, }; -use git_function_history::{BlockType, FileType, Filter}; +use git_function_history::{languages::Language, FileFilterType, Filter}; use std::{ fs, io::{Read, Write}, @@ -59,8 +58,8 @@ impl App { let mut history = String::new(); file.read_to_string(&mut history) .expect("Failed to read history file"); - let history = history.split('\n').map(|s| s.to_string()).collect(); - + let history: Vec = history.split('\n').map(|s| s.to_string()).collect(); + // let history_index = history.len(); let actions = vec![ Action::Quit, Action::TextEdit, @@ -82,8 +81,8 @@ impl App { body_height: 0, channels, status, + history_index: history.len() - 1, history, - history_index: 0, } } @@ -169,290 +168,199 @@ impl App { let mut iter = iter.trim().split(' '); match iter.next() { Some(cmd) => match cmd { - "filter" => { - if let CommandResult::History(_) = &self.cmd_output { + "search" => { + // check for a function name + if let Some(name) = iter.next() { + // check if there next arg stars with file or filter self.status = Status::Loading; - if let Some(filter) = iter.next() { - let filter = match filter { - "date" => { - if let Some(date) = iter.next() { - let date = date.replace('_', " "); - Some(Filter::Date(date)) - } else { - self.status = Status::Error("No date given".to_string()); - None - } + let mut file = FileFilterType::None; + let mut filter = Filter::None; + let mut lang = Language::All; + let new_vec = iter.collect::>(); + let mut new_iter = new_vec.windows(2); + log::debug!("searching for {:?}", new_iter); + + if new_vec.len() % 2 != 0 { + self.status = Status::Error(format!("uncomplete search, command: {} doesnt have its parameters",new_vec.last().expect("oops look like theres nothing in this vec don't how this happened"))); + return; + } + for i in &mut new_iter { + log::info!("i: {:?}", i); + match i { + ["relative", filepath] => { + log::trace!("relative file: {}", filepath); + file = FileFilterType::Relative(filepath.to_string()); } - "commit" => { - if let Some(commit) = iter.next() { - Some(Filter::CommitHash(commit.to_string())) - } else { - self.status = Status::Error("No commit given".to_string()); - None - } + ["absolute", filepath] => { + log::trace!("absolute file: {}", filepath); + file = FileFilterType::Absolute(filepath.to_string()); } - "parent" => { - if let Some(parent) = iter.next() { - Some(Filter::FunctionWithParent(parent.to_string())) - } else { - self.status = - Status::Error("No parent function given".to_string()); - None - } + ["date", date] => { + log::trace!("date: {}", date); + filter = Filter::Date(date.to_string()); } - "block" => { - if let Some(block) = iter.next() { - Some(Filter::FunctionInBlock(BlockType::from_string(block))) - } else { - self.status = - Status::Error("No block type given".to_string()); - None - } + ["commit", commit] => { + log::trace!("commit: {}", commit); + filter = Filter::CommitHash(commit.to_string()); } - "date-range" => { - if let Some(start) = iter.next() { - if let Some(end) = iter.next() { - // remove all - from the date - let start = start.replace('_', " "); - let end = end.replace('_', " "); - Some(Filter::DateRange(start, end)) - } else { - self.status = - Status::Error("No end date given".to_string()); - None + ["directory", dir] => { + log::trace!("directory: {}", dir); + file = FileFilterType::Directory(dir.to_string()); + } + ["date-range", pos] => { + log::trace!("date range: {}", pos); + let (start, end) = match pos.split_once("..") { + Some((start, end)) => (start, end), + None => { + self.status = Status::Error( + "Invalid date range, expected start..end" + .to_string(), + ); + return; } - } else { - self.status = - Status::Error("No start date given".to_string()); - None - } + }; + filter = Filter::DateRange(start.to_string(), end.to_string()); } - "line-range" => { - if let Some(start) = iter.next() { - if let Some(end) = iter.next() { - let start = match start.parse::() { - Ok(x) => x, - Err(e) => { - self.status = Status::Error(format!("{}", e)); - return; - } - }; - let end = match end.parse::() { - Ok(x) => x, - Err(e) => { - self.status = Status::Error(format!("{}", e)); - return; - } - }; - Some(Filter::FunctionInLines(start, end)) - } else { + ["language", language] => { + log::trace!("language: {}", language); + lang = match language { + &"rust" => Language::Rust, + &"python" => Language::Python, + // #[cfg(feature = "c_lang")] + // &"c" => Language::C, + #[cfg(feature = "unstable")] + &"go" => Language::Go, + &"ruby" => Language::Ruby, + _ => { self.status = - Status::Error("No end line given".to_string()); - None + Status::Error("Invalid language".to_string()); + return; } - } else { - self.status = - Status::Error("No start line given".to_string()); - None - } + }; } - "file-absolute" => { - if let Some(file) = iter.next() { - Some(Filter::FileAbsolute(file.to_string())) - } else { - self.status = Status::Error("No file given".to_string()); - None - } + ["author", author] => { + log::trace!("author: {}", author); + filter = Filter::Author(author.to_string()); } - "file-relative" => { - if let Some(file) = iter.next() { - Some(Filter::FileRelative(file.to_string())) - } else { - self.status = Status::Error("No file given".to_string()); - None - } + ["author-email", author_email] => { + log::trace!("author-email: {}", author_email); + filter = Filter::AuthorEmail(author_email.to_string()); } - "directory" => { - if let Some(dir) = iter.next() { - Some(Filter::Directory(dir.to_string())) - } else { - self.status = - Status::Error("No directory given".to_string()); - None - } + ["message", message] => { + log::trace!("message: {}", message); + filter = Filter::Message(message.to_string()); } + _ => { - self.status = Status::Error("Invalid filter".to_string()); - None + log::debug!("invalid arg: {i:?}"); + self.status = Status::Error(format!("Invalid search {i:?}")); + return; } - }; - if let Some(filter) = filter { - self.channels - .0 - .send(FullCommand::Filter(FilterType { - thing: self.cmd_output.clone(), - filter, - })) - .unwrap(); } - } else { - self.status = Status::Error("No filter given".to_string()); } - } else if iter.next().is_some() { - self.status = Status::Error("no filters available".to_string()); + self.channels + .0 + .send(FullCommand::Search(name.to_string(), file, filter, lang)) + .expect("could not send message in thread") + } else { + self.status = Status::Error("No function name given".to_string()); } } - "search" => { - // check for a function name - if let Some(name) = iter.next() { - // check if there next arg stars with file or filter - self.status = Status::Loading; - let search = match iter.next() { - None => { - // if there is no next arg then we are searching for a function - // with the given name - Some(FullCommand::Search( - name.to_string(), - FileType::None, - Filter::None, - )) + "filter" => { + self.status = Status::Loading; + let mut filter = Filter::None; + for i in &mut iter.clone().collect::>().windows(2) { + match i { + ["date", date] => { + filter = Filter::Date(date.to_string()); + } + ["commit", commit] => { + filter = Filter::CommitHash(commit.to_string()); + } + ["date-range", pos] => { + let (start, end) = match pos.split_once("..") { + Some((start, end)) => (start, end), + None => { + self.status = Status::Error( + "Invalid date range, expected start..end".to_string(), + ); + return; + } + }; + filter = Filter::DateRange(start.to_string(), end.to_string()); + } + ["author", author] => { + log::trace!("author: {}", author); + filter = Filter::Author(author.to_string()); + } + ["author-email", author_email] => { + log::trace!("author-email: {}", author_email); + filter = Filter::AuthorEmail(author_email.to_string()); + } + ["message", message] => { + log::trace!("message: {}", message); + filter = Filter::Message(message.to_string()); + } + ["line-range", pos] => { + // get the start and end by splitting the pos by: .. + let (start, end) = match pos.split_once("..") { + Some((start, end)) => (start, end), + None => { + self.status = Status::Error( + "Invalid line range, expected start..end".to_string(), + ); + return; + } + }; + let start = match start.parse::() { + Ok(x) => x, + Err(e) => { + self.status = Status::Error(format!("{e}")); + return; + } + }; + let end = match end.parse::() { + Ok(x) => x, + Err(e) => { + self.status = Status::Error(format!("{e}")); + return; + } + }; + filter = Filter::FunctionInLines(start, end); + } + ["file-absolute", file] => { + filter = Filter::FileAbsolute(file.to_string()); + } + ["file-relative", file] => { + filter = Filter::FileRelative(file.to_string()); + } + ["directory", dir] => { + filter = Filter::Directory(dir.to_string()); + } + _ => { + self.status = Status::Error(format!( + "Invalid filter {}", + i.first().unwrap_or(&"") + )); + return; } - Some(thing) => match thing { - "relative" | "absolute" => { - let file_type = match iter.next() { - Some(filter) => match thing { - "relative" => FileType::Relative(filter.to_string()), - "absolute" => FileType::Absolute(filter.to_string()), - _ => FileType::None, - }, - None => { - self.status = - Status::Error("No filter given".to_string()); - return; - } - }; - let filter = match iter.next() { - Some(filter) => match filter { - "date" => { - let date = iter.next(); - match date { - Some(date) => { - let date = date.replace('_', " "); - Filter::Date(date) - } - None => { - self.status = Status::Error( - "No date given".to_string(), - ); - return; - } - } - } - "commit" => { - let commit = iter.next(); - match commit { - Some(commit) => { - Filter::CommitHash(commit.to_string()) - } - None => { - self.status = Status::Error( - "No commit given".to_string(), - ); - return; - } - } - } - "date range" => { - let start = iter.next(); - let end = iter.next(); - match (start, end) { - (Some(start), Some(end)) => { - let start = start.replace('_', " "); - let end = end.replace('_', " "); - Filter::DateRange(start, end) - } - _ => { - self.status = Status::Error( - "No date range given".to_string(), - ); - return; - } - } - } - _ => { - self.status = - Status::Error("No filter given".to_string()); - return; - } - }, - None => Filter::None, - }; - - Some(FullCommand::Search(name.to_string(), file_type, filter)) - } - "date" | "commit" | "date range" => { - let filter = match thing { - "date" => { - let date = iter.next(); - match date { - Some(date) => Filter::Date(date.to_string()), - None => { - self.status = - Status::Error("No date given".to_string()); - return; - } - } - } - "commit" => { - let commit = iter.next(); - match commit { - Some(commit) => { - Filter::CommitHash(commit.to_string()) - } - None => { - self.status = Status::Error( - "No commit given".to_string(), - ); - return; - } - } - } - "date range" => { - let start = iter.next(); - let end = iter.next(); - match (start, end) { - (Some(start), Some(end)) => Filter::DateRange( - start.to_string(), - end.to_string(), - ), - _ => { - self.status = Status::Error( - "No date range given".to_string(), - ); - return; - } - } - } - _ => Filter::None, - }; - Some(FullCommand::Search( - name.to_string(), - FileType::None, - filter, - )) - } - _ => { - self.status = Status::Error("Invalid file type".to_string()); - None - } - }, - }; - if let Some(search) = search { - self.channels.0.send(search).unwrap(); } - } else { - self.status = Status::Error("No function name given".to_string()); } + if iter.clone().count() > 0 { + self.status = Status::Error(format!( + "Invalid filter, command: {:?} missing args", + iter.collect::>() + )); + return; + } + + self.channels + .0 + .send(FullCommand::Filter(FilterType { + thing: self.cmd_output.clone(), + filter, + })) + .expect("could not send message in thread") } "list" => { self.status = Status::Loading; @@ -471,11 +379,14 @@ impl App { } }; if let Some(list) = list { - self.channels.0.send(list).unwrap(); + self.channels + .0 + .send(list) + .expect("could not send message in thread") } } other => { - self.status = Status::Error(format!("Invalid command: {}", other)); + self.status = Status::Error(format!("Invalid command: {other}")); } }, None => { diff --git a/cargo-function-history/src/app/ui.rs b/cargo-function-history/src/app/ui.rs index 36462ad0..aabba481 100644 --- a/cargo-function-history/src/app/ui.rs +++ b/cargo-function-history/src/app/ui.rs @@ -1,13 +1,13 @@ use std::collections::BTreeMap; use function_history_backend_thread::types::Status; -use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; -use tui::style::{Color, Style}; -use tui::widgets::{Block, Borders, Paragraph}; -use tui::Frame; use tui::{ backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, text::{Span, Spans}, + widgets::{Block, Borders, Paragraph}, + Frame, }; use crate::app::App; @@ -52,8 +52,17 @@ where ) .split(whole_chunks); app.get_result(); - draw_body(app, body_chunks[0], rect); - let width = body_chunks[0].width.max(3) - 3; // keep 2 for borders and 1 for cursor + draw_body( + app, + *body_chunks.get(0).expect("could not get area to draw"), + rect, + ); + let width = body_chunks + .get(0) + .expect("could not get area to draw") + .width + .max(3) + - 3; // keep 2 for borders and 1 for cursor let scroll = (app.input_buffer.cursor() as u16).max(width) - width; let input = Paragraph::new(app.input_buffer.value()) .style(match app.state() { @@ -67,18 +76,25 @@ where .style(Style::default().fg(Color::White)), ) .scroll((0, scroll)); - rect.render_widget(input, body_chunks[1]); + rect.render_widget( + input, + *body_chunks.get(1).expect("could not get area to draw"), + ); if let AppState::Editing = app.state() { // AppState::Editing => { rect.set_cursor( // Put cursor past the end of the input text - body_chunks[1].x + (app.input_buffer.cursor() as u16).min(width), + body_chunks.get(1).expect("could not get area to draw").x + + (app.input_buffer.cursor() as u16).min(width), // Move one line down, from the border to the input line - body_chunks[1].y, + body_chunks.get(1).expect("could not get area to draw").y, ) } let status = draw_status(app.status()); - rect.render_widget(status, body_chunks[2]); + rect.render_widget( + status, + *body_chunks.get(2).expect("could not get area to draw"), + ); } fn draw_body(app: &mut App, mut pos: Rect, frame: &mut Frame) { @@ -118,7 +134,7 @@ fn draw_body(app: &mut App, mut pos: Rect, frame: &mut Frame) { a => a .to_string() .split('\n') - .map(|s| Spans::from(format!("{}\n", s))) + .map(|s| Spans::from(format!("{s}\n"))) .collect(), }; let body = Paragraph::new(tick_text) diff --git a/cargo-function-history/src/keys.rs b/cargo-function-history/src/keys.rs index 0c9eb226..08fe6740 100644 --- a/cargo-function-history/src/keys.rs +++ b/cargo-function-history/src/keys.rs @@ -99,7 +99,7 @@ impl Key { 10 => Key::F10, 11 => Key::F11, 12 => Key::F12, - _ => panic!("unknown function key: F{}", n), + _ => panic!("unknown function key: F{n}"), } } } @@ -110,11 +110,11 @@ impl Display for Key { Key::Alt(' ') => write!(f, "Alt+Space"), Key::Ctrl(' ') => write!(f, "Ctrl+Space"), Key::Char(' ') => write!(f, " "), - Key::Alt(c) => write!(f, "Alt+{}", c), - Key::Ctrl(c) => write!(f, "Ctrl+{}", c), - Key::Char(c) => write!(f, "{}", c), - Key::Shift(c) => write!(f, "Shift+{}", c), - _ => write!(f, "{:?}", self), + Key::Alt(c) => write!(f, "Alt+{c}"), + Key::Ctrl(c) => write!(f, "Ctrl+{c}"), + Key::Char(c) => write!(f, "{c}"), + Key::Shift(c) => write!(f, "Shift+{c}"), + _ => write!(f, "{self:?}"), } } } diff --git a/cargo-function-history/src/lib.rs b/cargo-function-history/src/lib.rs index 3164a007..8056742f 100644 --- a/cargo-function-history/src/lib.rs +++ b/cargo-function-history/src/lib.rs @@ -1,7 +1,6 @@ use std::{cell::RefCell, io::stdout, path::PathBuf, process::exit, rc::Rc, time::Duration}; -use crate::app::ui; -use app::{state::AppState, App, AppReturn}; +use app::{state::AppState, ui, App, AppReturn}; use crossterm::event::{self, Event, KeyCode}; use eyre::Result; use keys::Key; @@ -58,7 +57,7 @@ pub fn start_ui(app: Rc>) -> Result<()> { app.scroll_down(); } _ => { - input_backend::to_input_request(Event::Key(key)) + input_backend::to_input_request(&Event::Key(key)) .and_then(|req| app.input_buffer.handle(req)); } }, diff --git a/cargo-function-history/src/main.rs b/cargo-function-history/src/main.rs index 6a1b9638..eca1d556 100644 --- a/cargo-function-history/src/main.rs +++ b/cargo-function-history/src/main.rs @@ -2,11 +2,11 @@ use std::{cell::RefCell, env, error::Error, process::exit, rc::Rc, sync::mpsc}; use cargo_function_history::{app::App, start_ui}; use function_history_backend_thread::types::{FullCommand, Status}; -use git_function_history::{FileType, Filter}; +use git_function_history::{FileFilterType, Filter}; use log::info; fn main() -> Result<(), Box> { - simple_file_logger::init_logger("cargo_function_history", simple_file_logger::LogLevel::Info)?; + simple_file_logger::init_logger!("cargo_function_history")?; info!("Starting cargo function history"); let (tx_t, rx_m) = mpsc::channel(); let (tx_m, rx_t) = mpsc::channel(); @@ -16,7 +16,12 @@ fn main() -> Result<(), Box> { let status = match config.function_name { string if string.is_empty() => Status::Ok(None), string => { - tx_m.send(FullCommand::Search(string, config.file_type, config.filter))?; + tx_m.send(FullCommand::Search( + string, + config.file_type, + config.filter, + config.language, + ))?; Status::Loading } }; @@ -33,7 +38,10 @@ fn usage() -> ! { println!(" --file-relative - search any file ending with the filename specified after the function name"); println!(" --filter-date= - filter to the given date"); println!(" --filter-commit-hash= - filter to the given commit hash"); - println!(" --filter-date-range=: - filter to the given date range"); + println!(" --filter-date-range=: - filter to the given date range"); + println!(" --lang=[lang] - filter to the given language"); + println!(" Available languages: rust, python, c, all"); + println!(" Default: all"); exit(1); } @@ -41,21 +49,25 @@ fn usage() -> ! { struct Config { function_name: String, filter: Filter, - file_type: FileType, + file_type: FileFilterType, + language: git_function_history::languages::Language, } fn parse_args() -> Config { let mut config = Config { function_name: String::new(), filter: Filter::None, - file_type: FileType::None, + file_type: FileFilterType::None, + language: git_function_history::languages::Language::All, }; env::args().enumerate().skip(1).for_each(|arg| { if arg.0 == 1 { - println!("{}", arg.1); + if arg.1 == "--help" { + usage(); + } match arg.1.split_once(':') { Some(string_tuple) => { - config.file_type = FileType::Relative(string_tuple.1.replace('\\', "/")); + config.file_type = FileFilterType::Relative(string_tuple.1.replace('\\', "/")); config.function_name = string_tuple.0.to_string(); } None => { @@ -69,12 +81,12 @@ fn parse_args() -> Config { } "--file-absolute" => { match &config.file_type { - FileType::None => { + FileFilterType::None => { eprintln!("Error no file name specified"); exit(1); } - FileType::Relative(path) => { - config.file_type = FileType::Absolute(path.to_string()); + FileFilterType::Relative(path) => { + config.file_type = FileFilterType::Absolute(path.to_string()); } _ => {} @@ -82,12 +94,12 @@ fn parse_args() -> Config { } "--file-relative" => { match &config.file_type { - FileType::None => { + FileFilterType::None => { eprintln!("Error no file name specified"); exit(1); } - FileType::Absolute(path) => { - config.file_type = FileType::Relative(path.to_string()); + FileFilterType::Absolute(path) => { + config.file_type = FileFilterType::Relative(path.to_string()); } _ => {} } @@ -129,6 +141,34 @@ fn parse_args() -> Config { }; config.filter = Filter::DateRange(date_range.0.to_string(), date_range.1.to_string()); } + string if string.starts_with("--lang=") => { + let lang = match string.split('=').nth(1) { + Some(string) => string, + None => { + eprintln!("Error no language specified"); + exit(1); + } + }; + match lang { + "rust" => { + config.language = git_function_history::languages::Language::Rust; + } + "python" => { + config.language = git_function_history::languages::Language::Python; + } + #[cfg(feature = "c_lang")] + "c" => { + config.language = git_function_history::languages::Language::C; + } + "all" => { + config.language = git_function_history::languages::Language::All; + } + _ => { + eprintln!("Error invalid language specified"); + exit(1); + } + } + } _ => { println!("Error:\n\tUnknown argument: {}\n\tTip: use --help to see available arguments.", arg.1); exit(1); diff --git a/function_history_backend_thread/Cargo.toml b/function_history_backend_thread/Cargo.toml index 32992749..cd7ed9d4 100644 --- a/function_history_backend_thread/Cargo.toml +++ b/function_history_backend_thread/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "function_history_backend_thread" -version = "0.2.2" +version = "0.3.0" edition = "2021" license = "MIT" repository = "https://github.com/mendelsshop/git_function_history/tree/main/function_history_backend_thread" @@ -14,8 +14,9 @@ description = "threading and types for git-function-history" [features] default = ["parallel"] parallel = ["git_function_history/parallel"] -not-parallel = [] +# c_lang = ["git_function_history/c_lang"] +unstable = ["git_function_history/unstable"] [dependencies] -git_function_history = { path = "../git-function-history-lib", version = "0.6.2", default-features = false} -log = "0.4" +git_function_history = { path = "../git-function-history-lib", version = "0.7.0", default-features = false} +log = "0.4.17" diff --git a/function_history_backend_thread/README.md b/function_history_backend_thread/README.md index 67b52f06..7a59c665 100644 --- a/function_history_backend_thread/README.md +++ b/function_history_backend_thread/README.md @@ -1,3 +1,6 @@ +# [![crates.io](https://img.shields.io/crates/v/function_history_backend_thread.svg?label=latest%20version)](https://crates.io/crates/function_history_backend_thread) [![Crates.io](https://img.shields.io/crates/d/function_history_backend_thread?label=crates.io%20downloads)](function_history_backend_thread) ![msrv](../resources/msrv.svg) + + # Function History Backend Thread -Provides threading and custom types for [git-function-history-gui](https://github.com/mendelsshop/git_function_history/tree/main/git-function-history-gui) and [cargo-function-history](https://github.com/mendelsshop/git_function_history/tree/main/cargo-function-history) \ No newline at end of file +Provides threading and custom types for [git-function-history-gui](https://github.com/mendelsshop/git_function_history/tree/main/git-function-history-gui) and [cargo-function-history](https://github.com/mendelsshop/git_function_history/tree/main/cargo-function-history). diff --git a/function_history_backend_thread/src/lib.rs b/function_history_backend_thread/src/lib.rs index 9768c8c4..ec548f79 100644 --- a/function_history_backend_thread/src/lib.rs +++ b/function_history_backend_thread/src/lib.rs @@ -39,35 +39,36 @@ pub fn command_thread( log::info!("list"); } match list_type { - ListType::Commits => { - match git_function_history::get_git_commit_hashes() { - Ok(commits) => { - if log { - log::info!("found {} commits", commits.len()); - } - ( - CommandResult::String(commits), - Status::Ok(Some(format!( - "Found commits dates took {}s", - now.elapsed().as_secs() - ))), - ) + ListType::Commits => match git_function_history::get_git_info() { + Ok(commits) => { + if log { + log::info!("found {} commits", commits.len()); } - Err(err) => ( - CommandResult::None, - Status::Error(format!( - "Error getting commits: {} took {}s", - err, + let commits = + commits.iter().map(|c| c.hash.to_string()).collect(); + ( + CommandResult::String(commits), + Status::Ok(Some(format!( + "Found commits dates took {}s", now.elapsed().as_secs() - )), - ), + ))), + ) } - } - ListType::Dates => match git_function_history::get_git_dates() { + Err(err) => ( + CommandResult::None, + Status::Error(format!( + "Error getting commits: {} took {}s", + err, + now.elapsed().as_secs() + )), + ), + }, + ListType::Dates => match git_function_history::get_git_info() { Ok(dates) => { if log { log::info!("found {} dates", dates.len()); } + let dates = dates.iter().map(|d| d.date.to_rfc2822()).collect(); ( CommandResult::String(dates), Status::Ok(Some(format!( @@ -87,11 +88,17 @@ pub fn command_thread( }, } } - FullCommand::Search(name, file, filter) => { + FullCommand::Search(name, file, filter, lang) => { if log { - log::info!("Searching for {} in {:?}", name, file); + log::info!( + "Searching for {} in {:?} with language {} and filter {:?}", + name, + file, + lang, + filter + ); } - match get_function_history(&name, &file, filter) { + match get_function_history(&name, &file, &filter, &lang) { Ok(functions) => { if log { log::info!("Found functions"); @@ -155,7 +162,7 @@ pub fn command_thread( if log { log::info!("thread finished in {}s", now.elapsed().as_secs()); } - tx_t.send(msg).unwrap(); + tx_t.send(msg).expect("could not send message in thread") } } }); diff --git a/function_history_backend_thread/src/types.rs b/function_history_backend_thread/src/types.rs index 2fe81fa2..75107a0c 100644 --- a/function_history_backend_thread/src/types.rs +++ b/function_history_backend_thread/src/types.rs @@ -1,6 +1,6 @@ use std::fmt; -use git_function_history::{FileType, Filter, FunctionHistory}; +use git_function_history::{languages::Language, FileFilterType, Filter, FunctionHistory}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Command { @@ -73,11 +73,11 @@ impl fmt::Display for CommandResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CommandResult::History(history) => { - write!(f, "{}", history) + write!(f, "{history}") } CommandResult::String(string) => { for line in string { - writeln!(f, "{}", line)?; + writeln!(f, "{line}")?; } Ok(()) } @@ -98,11 +98,11 @@ impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Status::Ok(s) => match s { - Some(s) => write!(f, "Ok: {}", s), + Some(s) => write!(f, "Ok: {s}"), None => write!(f, "Ok"), }, - Status::Error(s) => write!(f, "Err {}", s), - Status::Warning(s) => write!(f, "Warn {}", s), + Status::Error(s) => write!(f, "Err {s}"), + Status::Warning(s) => write!(f, "Warn {s}"), Status::Loading => write!(f, "Loading..."), } } @@ -117,7 +117,7 @@ impl Default for Status { pub enum FullCommand { Filter(FilterType), List(ListType), - Search(String, FileType, Filter), + Search(String, FileFilterType, Filter, Language), } #[derive(Debug, Clone)] diff --git a/git-function-history-gui/Cargo.toml b/git-function-history-gui/Cargo.toml index c230ae98..165c0624 100644 --- a/git-function-history-gui/Cargo.toml +++ b/git-function-history-gui/Cargo.toml @@ -13,12 +13,13 @@ description = "GUI frontend for git-function-history" [features] default = ["parallel"] parallel = ["git_function_history/parallel", "function_history_backend_thread/parallel"] -not-parallel = [] +# c_lang = ["git_function_history/c_lang", "function_history_backend_thread/c_lang"] +unstable = ["git_function_history/unstable", "function_history_backend_thread/unstable"] [dependencies] eframe = {version = "0.20.1", features = ["dark-light"]} -git_function_history = { path = "../git-function-history-lib", version = "0.6.2", default-features = false} -function_history_backend_thread = { path = "../function_history_backend_thread", version = "0.2.2", default-features = false} +git_function_history = { path = "../git-function-history-lib", version = "0.7.0", default-features = false} +function_history_backend_thread = { path = "../function_history_backend_thread", version = "0.3.0", default-features = false} simple_file_logger = "0.3.1" log = "0.4.17" -image = "0.24" \ No newline at end of file +image = "0.24.5" \ No newline at end of file diff --git a/git-function-history-gui/README.md b/git-function-history-gui/README.md index 71a45114..c3fc3afa 100644 --- a/git-function-history-gui/README.md +++ b/git-function-history-gui/README.md @@ -1,4 +1,4 @@ -# [![crates.io](https://img.shields.io/crates/v/git-function-history-gui.svg?label=latest%20version)](https://crates.io/crates/git-function-history-gui) [![Crates.io](https://img.shields.io/crates/d/git-function-history-gui?label=crates.io%20downloads)](https://crates.io/crates/git-function-history-gui) +# [![crates.io](https://img.shields.io/crates/v/git-function-history-gui.svg?label=latest%20version)](https://crates.io/crates/git-function-history-gui) [![Crates.io](https://img.shields.io/crates/d/git-function-history-gui?label=crates.io%20downloads)](https://crates.io/crates/git-function-history-gui) ![msrv](../resources/msrv.svg) # git function history GUI @@ -26,7 +26,7 @@ Under Linux you may need to install the following packages: - libssl-dev On ubuntu you can install them with: -`sudo apt-get install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev` +`sudo apt install -y libclang-dev libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev` ## Usage diff --git a/git-function-history-gui/src/lib.rs b/git-function-history-gui/src/lib.rs index 9662c536..f43ff6cc 100644 --- a/git-function-history-gui/src/lib.rs +++ b/git-function-history-gui/src/lib.rs @@ -2,18 +2,14 @@ use std::{sync::mpsc, time::Duration}; use eframe::{ self, - egui::{self, Button, Layout, Sense, SidePanel}, - epaint::Vec2, -}; -use eframe::{ - egui::{Label, TextEdit, TopBottomPanel, Visuals}, - epaint::Color32, + egui::{self, Button, Label, Layout, Sense, SidePanel, TextEdit, TopBottomPanel, Visuals}, + epaint::{Color32, Vec2}, }; use function_history_backend_thread::types::{ Command, CommandResult, FilterType, FullCommand, HistoryFilterType, ListType, Status, }; use git_function_history::{ - types::Directions, BlockType, CommitFunctions, FileType, Filter, FunctionHistory, + languages::Language, types::Directions, Commit, FileFilterType, Filter, FunctionHistory, }; // TODO: stop cloning everyting and use references instead @@ -29,8 +25,11 @@ pub struct MyEguiApp { mpsc::Receiver<(CommandResult, Status)>, ), filter: Filter, - file_type: FileType, + file_type: FileFilterType, history_filter_type: HistoryFilterType, + language: Language, + current_commit: String, + do_commit: bool, } impl MyEguiApp { @@ -49,13 +48,26 @@ impl MyEguiApp { status: Status::default(), list_type: ListType::default(), channels, - file_type: FileType::None, + file_type: FileFilterType::None, filter: Filter::None, history_filter_type: HistoryFilterType::None, + language: Language::All, + current_commit: String::new(), + do_commit: false, } } - fn draw_commit(commit: &mut CommitFunctions, ctx: &egui::Context, show: bool) { + fn draw_config_window(&mut self, ctx: &egui::Context) { + egui::Window::new("My Window") + .open(&mut true) + .show(ctx, |ui| { + if ui.button("cancel").clicked() { + self.current_commit = String::new(); + } + }); + } + + fn draw_commit(commit: &mut Commit, ctx: &egui::Context, show: bool) { if show { TopBottomPanel::top("date_id").show(ctx, |ui| { ui.add(Label::new(format!( @@ -74,6 +86,7 @@ impl MyEguiApp { commit.get_metadata()["file"] ))); }); + let file = commit.get_file().map(|x| x.to_string()).unwrap_or_else(|| "error occured could not retrieve file please file a bug report to https://github.com/mendelsshop/git_function_history/issues".to_string()); match commit.get_move_direction() { Directions::None => { egui::CentralPanel::default().show(ctx, |ui| { @@ -82,7 +95,7 @@ impl MyEguiApp { .max_width(f32::INFINITY) .auto_shrink([false, false]) .show(ui, |ui| { - ui.add(Label::new(commit.get_file().to_string())); + ui.add(Label::new(file)); }); }); } @@ -103,7 +116,7 @@ impl MyEguiApp { .max_height(f32::INFINITY) .max_width(f32::INFINITY) .auto_shrink([false, false]) - .show(ui, |ui| ui.add(Label::new(commit.get_file().to_string()))); + .show(ui, |ui| ui.add(Label::new(file))); }); if resp.clicked() { commit.move_forward(); @@ -127,7 +140,7 @@ impl MyEguiApp { .max_width(f32::INFINITY) .auto_shrink([false, false]) .show(ui, |ui| { - ui.add(Label::new(commit.get_file().to_string())); + ui.add(Label::new(file)); }); }); if resp.clicked() { @@ -161,7 +174,7 @@ impl MyEguiApp { .max_width(f32::INFINITY) .auto_shrink([false, false]) .show(ui, |ui| { - ui.add(Label::new(commit.get_file().to_string())); + ui.add(Label::new(file)); }); }); if l_resp.clicked() { @@ -195,8 +208,14 @@ impl MyEguiApp { Vec2::new(ui.available_width() - max, 2.0), Label::new(format!( "{}\n{}", - history.get_metadata()["commit hash"], - history.get_metadata()["date"] + history + .get_metadata() + .get("commit hash") + .map_or("could not retrieve commit hash", |x| x.as_str()), + history + .get_metadata() + .get("date") + .map_or("could not retieve date", |x| x.as_str()), )), ); @@ -223,7 +242,13 @@ impl MyEguiApp { } }); }); - Self::draw_commit(history.get_mut_commit(), ctx, false); + if let Some(x) = history.get_mut_commit() { + Self::draw_commit(x, ctx, false) + } else { + TopBottomPanel::top("no_commit_found").show(ctx, |ui| { + ui.add(Label::new("no commit found")); + }); + } } } @@ -235,431 +260,544 @@ impl eframe::App for MyEguiApp { } else { ctx.set_visuals(Visuals::light()); } - egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { - ui.add_space(20.); - egui::menu::bar(ui, |ui| { - ui.with_layout( - Layout::left_to_right(eframe::emath::Align::Center), - |ui| match &self.status { - Status::Loading => { - ui.colored_label(Color32::BLUE, "Loading..."); - } - Status::Ok(a) => match a { - Some(a) => { - ui.colored_label(Color32::LIGHT_GREEN, format!("Ok: {}", a)); + if self.do_commit { + self.draw_config_window(ctx); + if self.current_commit.is_empty() { + self.do_commit = false; + } + } else { + egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { + ui.add_space(20.); + egui::menu::bar(ui, |ui| { + ui.with_layout(Layout::left_to_right(eframe::emath::Align::Center), |ui| { + match &self.status { + Status::Loading => { + ui.colored_label(Color32::BLUE, "Loading..."); } - None => { - ui.colored_label(Color32::GREEN, "Ready"); + Status::Ok(a) => match a { + Some(a) => { + ui.colored_label(Color32::LIGHT_GREEN, format!("Ok: {a}")); + } + None => { + ui.colored_label(Color32::GREEN, "Ready"); + } + }, + Status::Warning(a) => { + ui.colored_label(Color32::LIGHT_RED, format!("Warn: {a}")); + } + Status::Error(a) => { + ui.colored_label(Color32::LIGHT_RED, format!("Error: {a}")); } - }, - Status::Warning(a) => { - ui.colored_label(Color32::LIGHT_RED, format!("Warn: {}", a)); - } - Status::Error(a) => { - ui.colored_label(Color32::LIGHT_RED, format!("Error: {}", a)); } - }, - ); - // controls - ui.with_layout(Layout::right_to_left(eframe::emath::Align::Center), |ui| { - let theme_btn = ui.add(Button::new({ - if self.dark_theme { - "🌞" - } else { - "🌙" + }); + // controls + ui.with_layout(Layout::right_to_left(eframe::emath::Align::Center), |ui| { + let theme_btn = ui.add(Button::new({ + if self.dark_theme { + "🌞" + } else { + "🌙" + } + })); + if theme_btn.clicked() { + self.dark_theme = !self.dark_theme; } - })); - if theme_btn.clicked() { - self.dark_theme = !self.dark_theme; - } + }); }); + + ui.add_space(20.); }); + egui::TopBottomPanel::bottom("commnad_builder").show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + egui::ScrollArea::horizontal() + .max_height(f32::INFINITY) + .max_width(f32::INFINITY) + .auto_shrink([false, false]) + .show(ui, |ui| { + let max = ui.available_width() / 6.0; + egui::ComboBox::from_id_source("command_combo_box") + .selected_text(self.command.to_string()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.command, + Command::Filter, + "filter", + ); + ui.selectable_value( + &mut self.command, + Command::Search, + "search", + ); + ui.selectable_value(&mut self.command, Command::List, "list"); + }); + match self.command { + Command::Filter => { + match &self.cmd_output { + CommandResult::History(_) => { + // Options 1. by date 2. by commit hash 3. in date range 4. function in block 5. function in lines 6. function in function + let text = match &self.history_filter_type { + HistoryFilterType::None => { + "filter type".to_string() + } + a => a.to_string(), + }; + egui::ComboBox::from_id_source("history_combo_box") + .selected_text(text) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::Date(String::new()), + "by date", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::CommitHash(String::new()), + "by commit hash", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::DateRange( + String::new(), + String::new(), + ), + "in date range", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::FunctionInBlock( + String::new(), + ), + "function in block", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::FunctionInLines( + String::new(), + String::new(), + ), + "function in lines", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::FunctionInFunction( + String::new(), + ), + "function in function", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::FileAbsolute( + String::new(), + ), + "file absolute", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::FileRelative( + String::new(), + ), + "file relative", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::Directory(String::new()), + "directory", + ); + ui.selectable_value( + &mut self.history_filter_type, + HistoryFilterType::None, + "none", + ); + }); + match &mut self.history_filter_type { + HistoryFilterType::DateRange(line1, line2) + | HistoryFilterType::FunctionInLines( + line1, + line2, + ) => { + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(line1)); + }); + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(line2)); + }); + } + HistoryFilterType::Date(dir) + | HistoryFilterType::CommitHash(dir) + | HistoryFilterType::FunctionInBlock(dir) + | HistoryFilterType::FunctionInFunction(dir) + | HistoryFilterType::FileAbsolute(dir) + | HistoryFilterType::FileRelative(dir) + | HistoryFilterType::Directory(dir) => { + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(dir)); + }); + } + HistoryFilterType::None => { + // do nothing + } + } + let resp = ui.add(Button::new("Go")); + if resp.clicked() { + self.status = Status::Loading; + let filter = match &self.history_filter_type { + HistoryFilterType::Date(date) => { + Some(Filter::Date(date.to_string())) + } + HistoryFilterType::CommitHash(commit_hash) => { + Some(Filter::CommitHash( + commit_hash.to_string(), + )) + } + HistoryFilterType::DateRange(date1, date2) => { + Some(Filter::DateRange( + date1.to_string(), + date2.to_string(), + )) + } + HistoryFilterType::FunctionInBlock(_block) => { + None + } + // Some( + // Filter::FunctionInBlock(BlockType::from_string(block)), + // ), + HistoryFilterType::FunctionInLines( + line1, + line2, + ) => { + let fn_in_lines = ( + match line1.parse::() { + Ok(x) => x, + Err(e) => { + self.status = Status::Error( + format!("{e}"), + ); + return; + } + }, + match line2.parse::() { + Ok(x) => x, + Err(e) => { + self.status = Status::Error( + format!("{e}"), + ); + return; + } + }, + ); + Some(Filter::FunctionInLines( + fn_in_lines.0, + fn_in_lines.1, + )) + } + HistoryFilterType::FunctionInFunction( + _function, + ) => { + // Some(Filter::FunctionWithParent(function.to_string())) + None + } + HistoryFilterType::FileAbsolute(file) => { + Some(Filter::FileAbsolute(file.to_string())) + } + HistoryFilterType::FileRelative(file) => { + Some(Filter::FileRelative(file.to_string())) + } + HistoryFilterType::Directory(dir) => { + Some(Filter::Directory(dir.to_string())) + } + HistoryFilterType::None => { + self.status = Status::Ok(None); + None + } + }; + if let Some(filter) = filter { + self.channels + .0 + .send(FullCommand::Filter(FilterType { + thing: self.cmd_output.clone(), + filter, + })) + .expect("could not send message in thread"); + } + } + } - ui.add_space(20.); - }); - egui::TopBottomPanel::bottom("commnad_builder").show(ctx, |ui| { - egui::menu::bar(ui, |ui| { - let max = ui.available_width() / 6.0; - egui::ComboBox::from_id_source("command_combo_box") - .selected_text(self.command.to_string()) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.command, Command::Filter, "filter"); - ui.selectable_value(&mut self.command, Command::Search, "search"); - ui.selectable_value(&mut self.command, Command::List, "list"); - }); - match self.command { - Command::Filter => { - match &self.cmd_output { - CommandResult::History(_) => { - // Options 1. by date 2. by commit hash 3. in date range 4. function in block 5. function in lines 6. function in function - let text = match &self.history_filter_type { - HistoryFilterType::None => "filter type".to_string(), - a => a.to_string(), - }; - egui::ComboBox::from_id_source("history_combo_box") + _ => { + ui.add(Label::new("No filters available")); + } + } + } + Command::Search => { + ui.add(Label::new("Function Name:")); + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(&mut self.input_buffer)); + }); + + let text = match &self.file_type { + FileFilterType::Directory(_) => "directory", + FileFilterType::Absolute(_) => "absolute", + FileFilterType::Relative(_) => "relative", + _ => "file type", + }; + egui::ComboBox::from_id_source("search_file_combo_box") + .selected_text(text) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.file_type, + FileFilterType::None, + "None", + ); + ui.selectable_value( + &mut self.file_type, + FileFilterType::Relative(String::new()), + "Relative", + ); + ui.selectable_value( + &mut self.file_type, + FileFilterType::Absolute(String::new()), + "Absolute", + ); + ui.selectable_value( + &mut self.file_type, + FileFilterType::Directory(String::new()), + "Directory", + ); + }); + match &mut self.file_type { + FileFilterType::None => {} + FileFilterType::Relative(dir) + | FileFilterType::Absolute(dir) + | FileFilterType::Directory(dir) => { + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(dir)); + }); + } + } + // get filters if any + let text = match &self.filter { + Filter::CommitHash(_) => "commit hash".to_string(), + Filter::DateRange(..) => "date range".to_string(), + Filter::Date(_) => "date".to_string(), + _ => "filter type".to_string(), + }; + egui::ComboBox::from_id_source( + "search_search_filter_combo_box", + ) .selected_text(text) .show_ui(ui, |ui| { + ui.selectable_value(&mut self.filter, Filter::None, "None"); ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::Date(String::new()), - "by date", + &mut self.filter, + Filter::CommitHash(String::new()), + "Commit Hash", ); ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::CommitHash(String::new()), - "by commit hash", + &mut self.filter, + Filter::Date(String::new()), + "Date", ); ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::DateRange( - String::new(), - String::new(), - ), - "in date range", - ); - ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::FunctionInBlock(String::new()), - "function in block", - ); - ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::FunctionInLines( - String::new(), - String::new(), - ), - "function in lines", - ); - ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::FunctionInFunction(String::new()), - "function in function", - ); - ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::FileAbsolute(String::new()), - "file absolute", - ); - ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::FileRelative(String::new()), - "file relative", - ); - ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::Directory(String::new()), - "directory", - ); - ui.selectable_value( - &mut self.history_filter_type, - HistoryFilterType::None, - "none", + &mut self.filter, + Filter::DateRange(String::new(), String::new()), + "Date Range", ); }); - match &mut self.history_filter_type { - HistoryFilterType::DateRange(line1, line2) - | HistoryFilterType::FunctionInLines(line1, line2) => { - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(line1)); - }); - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(line2)); - }); + + // let + match &mut self.filter { + Filter::None => {} + Filter::CommitHash(thing) | Filter::Date(thing) => { + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(thing)); + }); + } + Filter::DateRange(start, end) => { + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(start)); + }); + ui.add(Label::new("-")); + ui.horizontal(|ui| { + // set the width of the input field + ui.set_min_width(4.0); + ui.set_max_width(max); + ui.add(TextEdit::singleline(end)); + }); + } + _ => {} } - HistoryFilterType::Date(dir) - | HistoryFilterType::CommitHash(dir) - | HistoryFilterType::FunctionInBlock(dir) - | HistoryFilterType::FunctionInFunction(dir) - | HistoryFilterType::FileAbsolute(dir) - | HistoryFilterType::FileRelative(dir) - | HistoryFilterType::Directory(dir) => { - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(dir)); + let text = self.language.to_string(); + egui::ComboBox::from_id_source("search_language_combo_box") + .selected_text(text) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.language, + Language::Rust, + "Rust", + ); + #[cfg(feature = "c_lang")] + ui.selectable_value( + &mut self.language, + Language::C, + "C", + ); + ui.selectable_value( + &mut self.language, + Language::Python, + "Python", + ); + ui.selectable_value( + &mut self.language, + Language::All, + "All", + ); }); - } - HistoryFilterType::None => { - // do nothing + let resp = ui.add(Button::new("Go")); + if resp.clicked() { + self.status = Status::Loading; + self.channels + .0 + .send(FullCommand::Search( + self.input_buffer.clone(), + self.file_type.clone(), + self.filter.clone(), + self.language, + )) + .expect("could not send message in thread"); } } - let resp = ui.add(Button::new("Go")); - if resp.clicked() { - self.status = Status::Loading; - let filter = match &self.history_filter_type { - HistoryFilterType::Date(date) => { - Some(Filter::Date(date.to_string())) - } - HistoryFilterType::CommitHash(commit_hash) => { - Some(Filter::CommitHash(commit_hash.to_string())) - } - HistoryFilterType::DateRange(date1, date2) => Some( - Filter::DateRange(date1.to_string(), date2.to_string()), - ), - HistoryFilterType::FunctionInBlock(block) => Some( - Filter::FunctionInBlock(BlockType::from_string(block)), - ), - HistoryFilterType::FunctionInLines(line1, line2) => { - let fn_in_lines = ( - match line1.parse::() { - Ok(x) => x, - Err(e) => { - self.status = - Status::Error(format!("{}", e)); - return; - } - }, - match line2.parse::() { - Ok(x) => x, - Err(e) => { - self.status = - Status::Error(format!("{}", e)); - return; - } - }, + Command::List => { + egui::ComboBox::from_id_source("list_type") + .selected_text(self.list_type.to_string()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.list_type, + ListType::Dates, + "dates", ); - Some(Filter::FunctionInLines( - fn_in_lines.0, - fn_in_lines.1, - )) - } - HistoryFilterType::FunctionInFunction(function) => { - Some(Filter::FunctionWithParent(function.to_string())) - } - HistoryFilterType::FileAbsolute(file) => { - Some(Filter::FileAbsolute(file.to_string())) - } - HistoryFilterType::FileRelative(file) => { - Some(Filter::FileRelative(file.to_string())) - } - HistoryFilterType::Directory(dir) => { - Some(Filter::Directory(dir.to_string())) - } - HistoryFilterType::None => { - self.status = Status::Ok(None); - None - } - }; - if let Some(filter) = filter { + ui.selectable_value( + &mut self.list_type, + ListType::Commits, + "commits", + ); + }); + let resp = ui.add(Button::new("Go")); + if resp.clicked() { + self.status = Status::Loading; self.channels .0 - .send(FullCommand::Filter(FilterType { - thing: self.cmd_output.clone(), - filter, - })) - .unwrap(); + .send(FullCommand::List(self.list_type)) + .expect("could not send message in thread"); } } } - - _ => { - ui.add(Label::new("No filters available")); - } - } - } - Command::Search => { - ui.add(Label::new("Function Name:")); - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(&mut self.input_buffer)); }); + }); + }); - let text = match &self.file_type { - FileType::Directory(_) => "directory", - FileType::Absolute(_) => "absolute", - FileType::Relative(_) => "relative", - _ => "file type", - }; - egui::ComboBox::from_id_source("search_file_combo_box") - .selected_text(text) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.file_type, FileType::None, "None"); - ui.selectable_value( - &mut self.file_type, - FileType::Relative(String::new()), - "Relative", - ); - ui.selectable_value( - &mut self.file_type, - FileType::Absolute(String::new()), - "Absolute", - ); - ui.selectable_value( - &mut self.file_type, - FileType::Directory(String::new()), - "Directory", - ); - }); - match &mut self.file_type { - FileType::None => {} - FileType::Relative(dir) - | FileType::Absolute(dir) - | FileType::Directory(dir) => { - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(dir)); - }); - } + egui::CentralPanel::default().show(ctx, |ui| { + // check if the channel has a message and if so set it to self.command + match self.channels.1.recv_timeout(Duration::from_millis(100)) { + Ok(timeout) => match timeout { + (_, Status::Error(e)) => { + let e = e.split_once("why").unwrap_or((&e, "")); + let e = format!( + "error recieved last command didn't work; {}{}", + e.0, + e.1.split_once("why").unwrap_or(("", "")).0, + ); + log::warn!("{}", e); + self.status = Status::Error(e); } - // get filters if any - let text = match &self.filter { - Filter::CommitHash(_) => "commit hash".to_string(), - Filter::DateRange(..) => "date range".to_string(), - Filter::Date(_) => "date".to_string(), - _ => "filter type".to_string(), - }; - egui::ComboBox::from_id_source("search_search_filter_combo_box") - .selected_text(text) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.filter, Filter::None, "None"); - ui.selectable_value( - &mut self.filter, - Filter::CommitHash(String::new()), - "Commit Hash", - ); - ui.selectable_value( - &mut self.filter, - Filter::Date(String::new()), - "Date", - ); - ui.selectable_value( - &mut self.filter, - Filter::DateRange(String::new(), String::new()), - "Date Range", - ); - }); - match &mut self.filter { - Filter::None => {} - Filter::CommitHash(thing) | Filter::Date(thing) => { - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(thing)); - }); - } - Filter::DateRange(start, end) => { - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(start)); - }); - ui.add(Label::new("-")); - ui.horizontal(|ui| { - // set the width of the input field - ui.set_min_width(4.0); - ui.set_max_width(max); - ui.add(TextEdit::singleline(end)); - }); - } - _ => {} + (t, Status::Ok(msg)) => { + log::info!("got results of last command"); + self.status = Status::Ok(msg); + self.cmd_output = t; } - let resp = ui.add(Button::new("Go")); - if resp.clicked() { - self.status = Status::Loading; - self.channels - .0 - .send(FullCommand::Search( - self.input_buffer.clone(), - self.file_type.clone(), - self.filter.clone(), - )) - .unwrap(); + _ => {} + }, + Err(e) => match e { + mpsc::RecvTimeoutError::Timeout => {} + mpsc::RecvTimeoutError::Disconnected => { + panic!("Disconnected"); } + }, + } + // match self.commmand and render based on that + match &mut self.cmd_output { + CommandResult::History(t) => { + Self::draw_history(t, ctx); } - Command::List => { - egui::ComboBox::from_id_source("list_type") - .selected_text(self.list_type.to_string()) - .show_ui(ui, |ui| { - ui.selectable_value(&mut self.list_type, ListType::Dates, "dates"); - ui.selectable_value( - &mut self.list_type, - ListType::Commits, - "commits", - ); + + CommandResult::String(t) => { + let resp = ui.button("go"); + egui::ScrollArea::vertical() + .max_height(f32::INFINITY) + .max_width(f32::INFINITY) + .auto_shrink([false, false]) + .show(ui, |ui| { + for line in t { + if !line.is_empty() { + // ui.add(Button::new(line.to_string())); + ui.selectable_value( + &mut self.current_commit, + line.to_string(), + line.to_string(), + ); + } + } }); - let resp = ui.add(Button::new("Go")); if resp.clicked() { - self.status = Status::Loading; - self.channels - .0 - .send(FullCommand::List(self.list_type)) - .unwrap(); + // show a popup window + // let response = ui.button("Open popup"); + // let popup_id = ui.make_persistent_id("my_unique_id"); + // // if response.clicked() { + // ui.memory().toggle_popup(popup_id); + // // } + // egui::popup::popup_below_widget(ui, popup_id, &resp, |ui| { + // ui.set_min_width(200.0); // if you want to control the size + // ui.label("Some more info, or things you can select:"); + // ui.label("…"); + // }); + // egui::Area::new("my_area") + // .fixed_pos(egui::pos2(32.0, 32.0)) + // .show(ctx, |ui| { + // ui.label("Floating text!"); + // }); + self.do_commit = true; } } - } + CommandResult::None => match &self.status { + Status::Loading => { + ui.add(Label::new("Loading...")); + } + _ => { + ui.add(Label::new("Nothing to show")); + ui.add(Label::new("Please select a command")); + } + }, + }; }); - }); - - egui::CentralPanel::default().show(ctx, |ui| { - // check if the channel has a message and if so set it to self.command - match self.channels.1.recv_timeout(Duration::from_millis(100)) { - Ok(timeout) => match timeout { - (_, Status::Error(e)) => { - let e = e.split_once("why").unwrap_or((&e, "")); - let e = format!( - "error recieved last command didn't work; {}{}", - e.0, - e.1.split_once("why").unwrap_or(("", "")).0, - ); - log::warn!("{}", e); - self.status = Status::Error(e); - } - (t, Status::Ok(msg)) => { - log::info!("got results of last command"); - self.status = Status::Ok(msg); - self.cmd_output = t; - } - _ => {} - }, - Err(e) => match e { - mpsc::RecvTimeoutError::Timeout => {} - mpsc::RecvTimeoutError::Disconnected => { - panic!("Disconnected"); - } - }, - } - // match self.commmand and render based on that - match &mut self.cmd_output { - CommandResult::History(t) => { - Self::draw_history(t, ctx); - } - - CommandResult::String(t) => { - egui::ScrollArea::vertical() - .max_height(f32::INFINITY) - .max_width(f32::INFINITY) - .auto_shrink([false, false]) - .show(ui, |ui| { - for line in t { - if !line.is_empty() { - ui.add(Label::new(line.to_string())); - } - } - }); - } - CommandResult::None => match &self.status { - Status::Loading => { - ui.add(Label::new("Loading...")); - } - _ => { - ui.add(Label::new("Nothing to show")); - ui.add(Label::new("Please select a command")); - } - }, - }; - }); + } } } diff --git a/git-function-history-gui/src/main.rs b/git-function-history-gui/src/main.rs index 2870f763..c71288bf 100644 --- a/git-function-history-gui/src/main.rs +++ b/git-function-history-gui/src/main.rs @@ -1,17 +1,15 @@ use eframe::{epaint::Vec2, run_native}; use git_function_history_gui::MyEguiApp; +use image::ImageFormat::Png; use std::sync::mpsc; fn main() { let (tx_t, rx_m) = mpsc::channel(); let (tx_m, rx_t) = mpsc::channel(); - simple_file_logger::init_logger( - "git-function-history-gui", - simple_file_logger::LogLevel::Info, - ) - .unwrap(); - use image::ImageFormat::Png; + simple_file_logger::init_logger!("git-function-history-gui") + .expect("could not intialize logger"); const ICON: &[u8] = include_bytes!("../resources/icon1.png"); - let icon = image::load_from_memory_with_format(ICON, Png).unwrap(); + let icon = + image::load_from_memory_with_format(ICON, Png).expect("could not load image for icon"); function_history_backend_thread::command_thread(rx_t, tx_t, true); let native_options = eframe::NativeOptions { initial_window_size: Some(Vec2::new(800.0, 600.0)), diff --git a/git-function-history-lib/Cargo.toml b/git-function-history-lib/Cargo.toml index 1bcf260e..0fa2869e 100644 --- a/git-function-history-lib/Cargo.toml +++ b/git-function-history-lib/Cargo.toml @@ -1,18 +1,33 @@ [package] name = "git_function_history" -version = "0.6.2" +version = "0.7.0" edition = "2021" license = "MIT" repository = "https://github.com/mendelsshop/git_function_history/tree/main/git-function-history-lib" keywords = ["git_function_history", "git", "function", ] categories = ["tools", "git"] description = "show function history from git" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["parallel"] -parallel = ["dep:rayon"] +default = ["parallel", "cache"] +parallel = ["dep:rayon", "git-features/parallel"] +# c_lang = [] +# unstable = ["dep:gosyn", "dep:javaparser"] +unstable = ["dep:gosyn"] +cache = ["dep:cached"] [dependencies] -chrono = "0.4.22" -ra_ap_syntax = "0.0.142" -rayon = { version = "1.5.1", optional = true } \ No newline at end of file +chrono = "0.4.23" +ra_ap_syntax = "0.0.148" +rayon = { version = "1.6.1", optional = true } +rustpython-parser = { features = ["lalrpop"], version = "0.2.0" } +lib-ruby-parser = "3.0.12" +gosyn = {version = "0.2.0", optional = true} +# can't be published b/c git dependency +# javaparser = {git = "https://github.com/tanin47/javaparser.rs", optional = true} +cfg-if = "1.0.0" +cached = {version = "0.42.0", optional = true} +gitoxide-core = "0.22.0" +git-repository = { version = "0.33.0", default-features = false, features = ["max-performance-safe"] } +git-features = { version = "0.26.1", features = ["zlib", "once_cell"] } diff --git a/git-function-history-lib/README.md b/git-function-history-lib/README.md index 8526302a..29516267 100644 --- a/git-function-history-lib/README.md +++ b/git-function-history-lib/README.md @@ -1,13 +1,27 @@ -# [![Clippy check + test](https://github.com/mendelsshop/git_function_history/actions/workflows/cargo_clippy_lib.yml/badge.svg)](https://github.com/mendelsshop/git_function_history/actions/workflows/cargo_clippy_lib.yml) [![crates.io](https://img.shields.io/crates/v/git_function_history.svg?label=latest%20version)](https://crates.io/crates/git_function_history) [![Crates.io](https://img.shields.io/crates/d/git_function_history?label=crates.io%20downloads)](https://crates.io/crates/git_function_history) [![docs.rs](https://img.shields.io/docsrs/git_function_history?logo=Docs.rs)](https://docs.rs/git_function_history/latest/git_function_history) +# [![Clippy check + test](https://github.com/mendelsshop/git_function_history/actions/workflows/cargo_clippy_lib.yml/badge.svg)](https://github.com/mendelsshop/git_function_history/actions/workflows/cargo_clippy_lib.yml) [![crates.io](https://img.shields.io/crates/v/git_function_history.svg?label=latest%20version)](https://crates.io/crates/git_function_history) [![Crates.io](https://img.shields.io/crates/d/git_function_history?label=crates.io%20downloads)](https://crates.io/crates/git_function_history) [![docs.rs](https://img.shields.io/docsrs/git_function_history?logo=Docs.rs)](https://docs.rs/git_function_history/latest/git_function_history) ![msrv](../resources/msrv.svg) # git function history Show the git history of a function or method. Use the latest (beta) version by putting `"git_function_history" = { git = 'https://github.com/mendelsshop/git_function_history' }` in your cargo.toml under `[dependencies]` section. -Use the latest [crates.io](https://crates.io/crates/git_function_history) by putting `git_function_history = "0.6.2"` in your cargo.toml under `[dependencies]` section. +Use the latest [crates.io](https://crates.io/crates/git_function_history) by putting `git_function_history = "0.7.0"` in your cargo.toml under `[dependencies]` section. ## features - parallel: use rayon to parallelize the git log search + - --no-default-features: disable parallelism + + + +- unstable: enable some parsers that require nightly rust so run `cargo +nightly` to use them + +- cache: enables caching when parsing files and folders that don't change as often. + +## parsing library dependencies + +| Language | Rust | Ruby | Python | Go | +| --- | --- | --- | --- | --- | +|Source| [ra_ap_syntax](https://crates.io/crates/ra_ap_syntax)([Rust Analyzer](https://rust-analyzer.github.io/)) | [lib-ruby-parser](https://crates.io/crates/lib-ruby-parser) | [rustpython-parser](https://crates.io/crates/rustpython-parser/)([RustPython](https://rustpython.github.io/)) | [gosyn](https://crates.io/crates/gosyn) | +| Requirements | | | | rust nightly and unstable feature | diff --git a/git-function-history-lib/src/languages/c.rs b/git-function-history-lib/src/languages/c.rs new file mode 100644 index 00000000..b4a24247 --- /dev/null +++ b/git-function-history-lib/src/languages/c.rs @@ -0,0 +1,129 @@ +use std::{error::Error, fmt}; + +use crate::impl_function_trait; + +use super::FunctionTrait; + +#[derive(Debug, Clone)] +pub struct CFunction { + pub(crate) name: String, + pub(crate) body: String, + pub(crate) parameters: Vec, + pub(crate) parent: Vec, + pub(crate) returns: Option, + pub(crate) lines: (usize, usize), +} + +impl CFunction { + pub fn new( + name: String, + body: String, + parameters: Vec, + parent: Vec, + returns: Option, + lines: (usize, usize), + ) -> Self { + Self { + name, + body, + parameters, + parent, + returns, + lines, + } + } +} + +impl fmt::Display for CFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + if !self.parameters.is_empty() { + write!(f, "(")?; + for (i, param) in self.parameters.iter().enumerate() { + if i != 0 { + write!(f, ", ")?; + } + write!(f, "{}", param)?; + } + write!(f, ")")?; + } + if let Some(ret) = &self.returns { + write!(f, " -> {}", ret)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct ParentFunction { + pub(crate) name: String, + pub(crate) top: String, + pub(crate) bottom: String, + pub(crate) lines: (usize, usize), + pub(crate) parameters: Vec, + pub(crate) returns: Option, +} +#[inline] + +pub(crate) fn find_function_in_file( + file_contents: &str, + name: &str, +) -> Result, Box> { + println!("Finding function {} in commit {}", name, file_contents); + + todo!("find_function_in_commit") +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CFilter { + /// when you want filter by a function that has a parent function of a specific name + FunctionWithParent(String), + /// when you want to filter by a function that has a has a specific return type + FunctionWithReturnType(String), +} + +impl CFilter { + pub fn matches(&self, function: &CFunction) -> bool { + match self { + Self::FunctionWithParent(parent) => function + .parent + .iter() + .any(|parent_function| parent_function.name == *parent), + Self::FunctionWithReturnType(return_type) => function + .returns + .as_ref() + .map_or(false, |r| r == return_type), + } + } +} + +impl FunctionTrait for CFunction { + fn get_total_lines(&self) -> (usize, usize) { + let mut start = self.lines.0; + let mut end = self.lines.1; + for parent in &self.parent { + if parent.lines.0 < start { + start = parent.lines.0; + end = parent.lines.1; + } + } + (start, end) + } + + fn get_tops(&self) -> Vec { + let mut tops = Vec::new(); + for parent in &self.parent { + tops.push(parent.top.clone()); + } + tops + } + + fn get_bottoms(&self) -> Vec { + let mut bottoms = Vec::new(); + for parent in &self.parent { + bottoms.push(parent.bottom.clone()); + } + bottoms + } + + impl_function_trait!(CFunction); +} diff --git a/git-function-history-lib/src/languages/go.rs b/git-function-history-lib/src/languages/go.rs new file mode 100644 index 00000000..67fa7051 --- /dev/null +++ b/git-function-history-lib/src/languages/go.rs @@ -0,0 +1,228 @@ +use crate::impl_function_trait; +use std::{collections::HashMap, error::Error, fmt}; + +use super::FunctionTrait; + +#[derive(Debug, Clone)] +/// A Go function +pub struct GoFunction { + pub(crate) name: String, + pub(crate) body: String, + pub(crate) parameters: GoParameter, + pub(crate) returns: Option, + pub(crate) lines: (usize, usize), +} +#[derive(Debug, Clone)] +/// The parameters of a Go function +pub enum GoParameter { + /// type parameter + Type(Vec), + /// named parameter, still has a type (name, type) + Named(HashMap), +} + +impl GoParameter { + pub fn extend(&mut self, other: &Self) { + match (self, other) { + (Self::Type(a), Self::Type(b)) => a.extend(b.clone()), + (Self::Named(a), Self::Named(b)) => a.extend(b.clone()), + _ => {} + } + } +} + +impl GoFunction { + pub const fn new( + name: String, + body: String, + parameters: GoParameter, + returns: Option, + lines: (usize, usize), + ) -> Self { + Self { + name, + body, + parameters, + returns, + lines, + } + } +} + +impl fmt::Display for GoFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.body)?; + Ok(()) + } +} + +impl FunctionTrait for GoFunction { + impl_function_trait!(GoFunction); + + fn get_tops(&self) -> Vec<(String, usize)> { + vec![] + } + + fn get_bottoms(&self) -> Vec<(String, usize)> { + vec![] + } + + fn get_total_lines(&self) -> (usize, usize) { + let start = self.lines.0; + let end = self.lines.1; + (start, end) + } +} + +pub(crate) fn find_function_in_file( + file_contents: &str, + name: &str, +) -> Result, Box> { + let parsed_file = gosyn::parse_source(file_contents) + .map_err(|e| format!("{e:?}"))? + .decl; + let parsed = parsed_file + .into_iter() + .filter_map(|decl| match decl { + gosyn::ast::Declaration::Function(func) => { + if func.name.name == name { + let mut lines = match func.body.as_ref() { + Some(body) => (func.name.pos, body.pos.1), + None => return None, + }; + if let Some(recv) = func.recv { + lines.0 = recv.pos(); + } + // FIXME: make sure that func is not commented out + lines.0 = file_contents + .get(..lines.0) + .map_or(lines.0, |c| c.rfind("func").unwrap_or(lines.0)); + for i in &func.docs { + if i.pos < lines.0 { + lines.0 = i.pos; + } + } + let mut body = file_contents + .get(lines.0..=lines.1)? + .to_string() + .trim_end() + .to_string(); + let index = super::turn_into_index(file_contents).ok()?; + lines.1 = super::get_from_index(&index, lines.1)?; + lines.0 = super::get_from_index(&index, lines.0)?; + + let start = lines.0; + body = super::make_lined(&body, start); + // see if the first parameter has a name: + let mut parameters = func.typ.params.list.get(0).map_or_else( + || GoParameter::Type(vec![]), + |param| { + if param.name.is_empty() { + GoParameter::Type(match ¶m.typ { + gosyn::ast::Expression::Ident(ident) => { + vec![ident.name.clone()] + } + _ => { + vec![] + } + }) + } else { + let typ = match ¶m.typ { + gosyn::ast::Expression::Ident(ident) => ident.name.clone(), + + _ => String::new(), + }; + let names = param.name.iter().map(|n| n.name.clone()); + GoParameter::Named( + names.into_iter().map(|name| (name, typ.clone())).collect(), + ) + } + }, + ); + + func.typ.params.list.iter().skip(1).for_each(|param| { + if param.name.is_empty() { + if let gosyn::ast::Expression::Ident(ident) = ¶m.typ { + if let GoParameter::Type(types) = &mut parameters { + types.push(ident.name.clone()); + } + } + } else { + let typ = match ¶m.typ { + gosyn::ast::Expression::Ident(ident) => ident.name.clone(), + + _ => String::new(), + }; + let names = param.name.iter().map(|n| n.name.clone()); + + if let GoParameter::Named(named) = &mut parameters { + for name in names { + named.insert(name, typ.clone()); + } + } + } + }); + let returns = Some( + func.typ + .result + .list + .iter() + .map(|p| { + p.name + .iter() + .map(|x| &x.name) + .map(std::string::ToString::to_string) + .collect::() + }) + .collect(), + ) + .filter(|x: &String| !x.is_empty()); + Some(GoFunction::new( + func.name.name, + body, + parameters, + returns, + lines, + )) + } else { + None + } + } + _ => None, + }) + .collect::>(); + if parsed.is_empty() { + return Err(format!("could not find function {name} in file"))?; + } + + Ok(parsed) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GoFilter { + // refers to the type of a parameter of a function + HasParameter(String), + // refers to the name of a parameter of a function + HasParameterName(String), + // refers to the type of the return value of a function + HasReturnType(String), +} + +impl GoFilter { + pub fn matches(&self, func: &GoFunction) -> bool { + match self { + Self::HasParameter(param) => match &func.parameters { + GoParameter::Type(types) => types.iter().any(|t| t == param), + GoParameter::Named(named) => named.values().any(|t| t == param), + }, + Self::HasParameterName(param) => { + if let GoParameter::Named(named) = &func.parameters { + named.iter().any(|(name, _)| name == param) + } else { + false + } + } + Self::HasReturnType(ret) => func.returns.as_ref().map_or(false, |x| x.contains(ret)), + } + } +} diff --git a/git-function-history-lib/src/languages/java.rs b/git-function-history-lib/src/languages/java.rs new file mode 100644 index 00000000..eb13dde2 --- /dev/null +++ b/git-function-history-lib/src/languages/java.rs @@ -0,0 +1,331 @@ +use std::fmt; + +use javaparser::{analyze::definition::MethodDef, parse::tree::CompilationUnitItem}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JavaFunction { + pub name: String, + pub lines: (usize, usize), + pub args: Vec, + pub body: String, + pub class: Vec, +} + +impl JavaFunction { + pub fn new( + name: String, + lines: (usize, usize), + args: Vec, + body: String, + class: Vec, + ) -> Self { + Self { + name, + lines, + args, + body, + class, + } + } +} + +impl fmt::Display for JavaFunction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: sort top and bottom by line number + write!( + f, + "{}", + self.class + .iter() + .map(|c| format!("{}\n...\n", c.top)) + .collect::() + )?; + write!(f, "{}", self.body)?; + write!( + f, + "{}", + self.class + .iter() + .rev() + .map(|c| format!("\n...\n{}", c.bottom)) + .collect::() + )?; + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JavaBlock { + pub name: String, + pub line: (usize, usize), + pub top: String, + pub bottom: String, + pub type_: JavaBlockType, +} +#[derive(Debug, Clone, PartialEq, Eq, Copy)] +pub enum JavaBlockType { + Class, + Enum, + Interface, +} + +pub(crate) fn find_function_in_file( + file_contents: &str, + name: &str, +) -> Result, String> { + let file = javaparser::parse::apply(file_contents, "").map_err(|_| "Parse error")?; + let parsed = file.unit.clone().items; + let mut functions = Vec::new(); + for unit in parsed { + extract_methods_from_compilation_unit(&unit, name).map(|f| functions.extend(f))?; + } + Ok(functions) +} + +fn extract_methods_from_compilation_unit( + unit: &CompilationUnitItem<'_>, + name: &str, +) -> Result, String> { + // recursively search for items with type Method + let mut methods = Vec::new(); + + match unit { + javaparser::parse::tree::CompilationUnitItem::Class(class) => { + let mut class_methods = Vec::new(); + for item in &class.body.items { + extract_methods_from_class_item(item, name).map(|f| class_methods.extend(f))?; + } + methods.extend(class_methods); + } + javaparser::parse::tree::CompilationUnitItem::Interface(interface) => { + let mut interface_methods = Vec::new(); + for item in &interface.body.items { + extract_methods_from_class_item(item, name).map(|f| interface_methods.extend(f))?; + } + methods.extend(interface_methods); + } + javaparser::parse::tree::CompilationUnitItem::Enum(enum_) => { + let mut enum_methods = Vec::new(); + if let Some(enum_body) = &enum_.body_opt { + for item in &enum_body.items { + extract_methods_from_class_item(item, name).map(|f| enum_methods.extend(f))?; + } + } + methods.extend(enum_methods); + } + javaparser::parse::tree::CompilationUnitItem::Annotation(annotation) => { + let mut annotation_methods = Vec::new(); + for item in &annotation.body.items { + extract_methods_from_annotation_item(item, name) + .map(|f| annotation_methods.extend(f))?; + } + methods.extend(annotation_methods); + } + } + Ok(methods) +} + +fn extract_methods_from_class_item( + item: &javaparser::parse::tree::ClassBodyItem<'_>, + name: &str, +) -> Result, String> { + let mut methods = Vec::new(); + match item { + javaparser::parse::tree::ClassBodyItem::Method(method) => { + println!("{:#?}", method); + println!("Found method: {}", method.name.fragment); + if method.name.fragment == name { + // let methdef = javaparser::analyze::build::method::build(method); + // let def = javaparser::extract::Definition::Method(methdef); + // println!("{:#?}", methdef.span_opt.map(|s| s.fragment)); + // println!("{:#?}", methdef); + let args = vec![]; + // method + // .parameters + // .iter() + // .map(|p| p.name.to_string()) + // .collect::>(); + // let body = method.body.to_string(); + // let lines = (method.line, method.line + body.lines().count()); + // println!("{:#?}", method); + // method. + // to find the the bottom of the class see if block_opt is some then find the span of the last block find the first } after that and then find the line number of that + let mut top = 0; + method.modifiers.iter().for_each(|m| { + //match the modifier to extract the line number + match m { + javaparser::parse::tree::Modifier::Keyword(k) => { + top = k.name.line; + } + javaparser::parse::tree::Modifier::Annotated(a) => { + match a { + javaparser::parse::tree::Annotated::Normal(n) => { + n.params.iter().for_each(|p| { + if p.name.line > top { + top = p.name.line; + } + }); + } + javaparser::parse::tree::Annotated::Marker(m) => { + // top = m.name.line; + } + javaparser::parse::tree::Annotated::Single(s) => { + // top = s.name.line; + } + } + } + } + }); + if top == 0 { + top = match method.return_type.span_opt() { + Some(s) => s.line, + None => return Err("could not find top of method")?, + } + } + let mut bottom = 0; + // to find the top of the class find the first { before the method and then find the line number of that first check modifiers if not use return type + + if let Some(b) = &method.block_opt { + // find the last block and then find the line number of the first } after that + } + // if there is no block then find the line number of + + let class = Vec::new(); + methods.push(JavaFunction::new( + "test".to_string(), + (0, 0), + args, + "test".to_string(), + class, + )); + } + } + javaparser::parse::tree::ClassBodyItem::Class(class) => { + let mut class_methods = Vec::new(); + for item in &class.body.items { + extract_methods_from_class_item(item, name).map(|f| class_methods.extend(f))?; + } + methods.extend(class_methods); + } + javaparser::parse::tree::ClassBodyItem::Interface(interface) => { + let mut interface_methods = Vec::new(); + for item in &interface.body.items { + extract_methods_from_class_item(item, name).map(|f| interface_methods.extend(f))?; + } + methods.extend(interface_methods); + } + javaparser::parse::tree::ClassBodyItem::Enum(enum_) => { + let mut enum_methods = Vec::new(); + if let Some(enum_body) = &enum_.body_opt { + for item in &enum_body.items { + extract_methods_from_class_item(item, name).map(|f| enum_methods.extend(f))?; + } + } + methods.extend(enum_methods); + } + javaparser::parse::tree::ClassBodyItem::Annotation(annotation) => { + let mut annotation_methods = Vec::new(); + for item in &annotation.body.items { + extract_methods_from_annotation_item(item, name) + .map(|f| annotation_methods.extend(f))?; + } + methods.extend(annotation_methods); + } + javaparser::parse::tree::ClassBodyItem::Constructor(_) + | javaparser::parse::tree::ClassBodyItem::FieldDeclarators(_) + | javaparser::parse::tree::ClassBodyItem::StaticInitializer(_) => {} + } + Ok(methods) +} + +fn extract_methods_from_annotation_item( + item: &javaparser::parse::tree::AnnotationBodyItem<'_>, + name: &str, +) -> Result, String> { + let mut methods = Vec::new(); + match item { + javaparser::parse::tree::AnnotationBodyItem::Annotation(annotation) => { + let mut annotation_methods = Vec::new(); + for item in &annotation.body.items { + extract_methods_from_annotation_item(item, name) + .map(|f| annotation_methods.extend(f))?; + } + methods.extend(annotation_methods); + } + + javaparser::parse::tree::AnnotationBodyItem::Class(class) => { + let mut class_methods = Vec::new(); + for item in &class.body.items { + extract_methods_from_class_item(item, name).map(|f| class_methods.extend(f))?; + } + methods.extend(class_methods); + } + javaparser::parse::tree::AnnotationBodyItem::Interface(interface) => { + let mut interface_methods = Vec::new(); + for item in &interface.body.items { + extract_methods_from_class_item(item, name).map(|f| interface_methods.extend(f))?; + } + methods.extend(interface_methods); + } + javaparser::parse::tree::AnnotationBodyItem::Enum(enum_) => { + let mut enum_methods = Vec::new(); + if let Some(enum_body) = &enum_.body_opt { + for item in &enum_body.items { + extract_methods_from_class_item(item, name).map(|f| enum_methods.extend(f))?; + } + } + methods.extend(enum_methods); + } + javaparser::parse::tree::AnnotationBodyItem::Param(_) + | javaparser::parse::tree::AnnotationBodyItem::FieldDeclarators(_) => {} + } + Ok(methods) +} + +#[cfg(test)] +mod java_test { + use super::*; + + #[test] + fn java() { + let file_contents = r#" + @Company + public class Test { + void main(String[] args) { + // System.out.println("Hello, World"); + } + } + "#; + let function = find_function_in_file(file_contents, "main").unwrap(); + println!("{:#?}", function); + } + + #[test] + fn java_fn_print() { + let java_class1 = JavaBlock { + name: "Test".to_string(), + line: (1, 1), + top: "1: public class Test {".to_string(), + bottom: "30: }".to_string(), + type_: JavaBlockType::Class, + }; + let java_class2 = JavaBlock { + name: "Test2".to_string(), + line: (1, 1), + top: "3: public class Test2 {".to_string(), + bottom: "28: }".to_string(), + type_: JavaBlockType::Class, + }; + let java_fn = JavaFunction::new( + "main".to_string(), + (1, 1), + vec![], + "5: public static void main(String[] args) { +7: System.out.println(\"Hello, World\"); +8: }" + .to_string(), + vec![java_class1, java_class2], + ); + println!("{}", java_fn); + } +} diff --git a/git-function-history-lib/src/languages/mod.rs b/git-function-history-lib/src/languages/mod.rs new file mode 100644 index 00000000..43ed6659 --- /dev/null +++ b/git-function-history-lib/src/languages/mod.rs @@ -0,0 +1,372 @@ +use crate::{Filter, UnwrapToError}; +use std::{ + collections::HashMap, + error::Error, + fmt::{self}, +}; +// TODO: lisp/scheme js, java?(https://github.com/tanin47/javaparser.rs) php?(https://docs.rs/tagua-parser/0.1.0/tagua_parser/) +use self::{python::PythonFunction, ruby::RubyFunction, rust::RustFunction}; + +// #[cfg(feature = "c_lang")] +// use self::c::CFunction; + +#[cfg(feature = "unstable")] +use go::GoFunction; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Language { + /// The python language + Python, + /// The rust language + Rust, + // #[cfg(feature = "c_lang")] + // /// c language + // C, + #[cfg(feature = "unstable")] + /// The go language + Go, + /// the Ruby language + Ruby, + /// all available languages + All, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LanguageFilter { + /// python filter + Python(python::PythonFilter), + /// rust filter + Rust(rust::RustFilter), + // #[cfg(feature = "c_lang")] + // /// c filter + // C(c::CFilter), + #[cfg(feature = "unstable")] + /// go filter + Go(go::GoFilter), + /// ruby filter + Ruby(ruby::RubyFilter), +} + +impl Language { + /// takes string and returns the corresponding language + /// + /// # Errors + /// + /// `Err` will be returned if the string is not a valid language + pub fn from_string(s: &str) -> Result> { + match s { + "python" => Ok(Self::Python), + "rust" => Ok(Self::Rust), + // #[cfg(feature = "c_lang")] + // "c" => Ok(Self::C), + #[cfg(feature = "unstable")] + "go" => Ok(Self::Go), + "all" => Ok(Self::All), + "ruby" => Ok(Self::Ruby), + _ => Err(format!("Unknown language: {s}"))?, + } + } + + /// returns the name of the language(s) + pub const fn get_names(&self) -> &str { + match self { + Self::Python => "python", + Self::Rust => "rust", + // #[cfg(feature = "c_lang")] + // Language::C => "c", + #[cfg(feature = "unstable")] + Self::Go => "go", + Self::Ruby => "ruby", + #[cfg(feature = "unstable")] + Self::All => "python, rust, go, or ruby", + #[cfg(not(feature = "unstable"))] + Self::All => "python, rust, or ruby", + } + } + + /// returns the file extensions of the language(s) + pub const fn get_file_endings(&self) -> &[&str] { + match self { + Self::Python => &["py", "pyw"], + Self::Rust => &["rs"], + // #[cfg(feature = "c_lang")] + // Language::C => &["c", "h"], + #[cfg(feature = "unstable")] + Self::Go => &["go"], + Self::Ruby => &["rb"], + #[cfg(feature = "unstable")] + Self::All => &["py", "pyw", "rs", "go", "rb"], + #[cfg(not(feature = "unstable"))] + Self::All => &["py", "pyw", "rs", "rb"], + } + } +} + +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Python => write!(f, "python"), + Self::Rust => write!(f, "rust"), + // #[cfg(feature = "c_lang")] + // Self::C => write!(f, "c"), + #[cfg(feature = "unstable")] + Self::Go => write!(f, "go"), + Self::Ruby => write!(f, "ruby"), + Self::All => write!(f, "all"), + } + } +} +// #[cfg(feature = "c_lang")] +// pub mod c; +#[cfg(feature = "unstable")] +pub mod go; +// #[cfg(feature = "unstable")] +// pub mod java; +pub mod python; +pub mod ruby; +pub mod rust; + +/// trait that all languages functions must implement +pub trait FunctionTrait: fmt::Debug + fmt::Display { + /// returns the starting and ending line of the function + fn get_lines(&self) -> (usize, usize); + /// returns the starting and ending line of the the function including any class/impls (among others) the function is part of + fn get_total_lines(&self) -> (usize, usize); + /// returns the name of the function + fn get_name(&self) -> String; + /// returns the body of the function (the whole function including its signature and end) + fn get_body(&self) -> String; + /// returns the tops like any the heading of classes/impls (among others) the function is part of along with the starting line of each heading + /// for example it could return `[("impl Test {", 3)]` + /// to get just for example the headings use the map method `function.get_tops().map(|top| top.0)` + fn get_tops(&self) -> Vec<(String, usize)>; + /// same as `get_tops` just retrieves the bottoms like so `[("}", 22)]` + fn get_bottoms(&self) -> Vec<(String, usize)>; +} + +// mace macro that generates get_lines, get_body,get_name +#[macro_export] +macro_rules! impl_function_trait { + ($name:ident) => { + fn get_lines(&self) -> (usize, usize) { + self.lines + } + + fn get_name(&self) -> String { + self.name.clone() + } + fn get_body(&self) -> String { + self.body.to_string() + } + }; +} + +fn make_lined(snippet: &str, mut start: usize) -> String { + snippet + .lines() + .map(|line| { + let new = format!("{start}: {line}\n"); + start += 1; + new + }) + .collect::() + .trim_end() + .to_string() +} + +/// trait that all languages files must implement +pub trait FileTrait: fmt::Debug + fmt::Display { + /// returns the language of the file + fn get_language(&self) -> Language; + /// returns the name of the file + fn get_file_name(&self) -> String; + /// returns the found functions in the file + fn get_functions(&self) -> Vec>; + + /// # Errors + /// + /// returns `Err` if the wrong filter is given, only `PLFilter` and `FunctionInLines` variants of `Filter` are valid. + /// with `PLFilter` it will return `Err` if you mismatch the file type with the filter Ie: using `RustFile` and `PythonFilter` will return `Err`. + fn filter_by(&self, filter: &Filter) -> Result> + where + Self: Sized; + fn get_current(&self) -> Option>; +} + +fn turn_into_index(snippet: &str) -> Result>, Box> { + // turn snippet into a hashmap of line number to char index + // so line 1 is 0 to 10, line 2 is 11 to 20, etc + let mut index = HashMap::new(); + index.insert(1, vec![]); + let mut line: usize = 1; + let mut char_index: usize = 0; + for c in snippet.chars() { + if c == '\n' { + line += 1; + index.insert(line, vec![char_index]); + } else { + index + .get_mut(&line) + .unwrap_to_error("line not found")? + .push(char_index); + } + char_index += c.len_utf8(); + } + Ok(index) +} + +fn get_from_index(index: &HashMap>, char: usize) -> Option { + // gets the line number from the index + index + .iter() + .find(|(_, v)| v.contains(&char)) + .map(|(k, _)| *k) +} + +// macro that generates the code for the different languages +macro_rules! make_file { + ($name:ident, $function:ident, $filtername:ident) => { + #[derive(Debug, Clone)] + pub struct $name { + file_name: String, + functions: Vec<$function>, + current_pos: usize, + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut file: Vec<(String, usize)> = Vec::new(); + for function in &self.functions { + // get the tops and their starting line number ie: parentfn.lines.0 + file.extend(function.get_tops()); + file.push((function.body.to_string(), function.get_lines().0)); + // get the bottoms and their end line number ie: parentfn.lines.1 + file.extend(function.get_bottoms()); + } + file.sort_by(|a, b| a.1.cmp(&b.1)); + file.dedup(); + // order the file by line number + file.sort_by(|a, b| a.1.cmp(&b.1)); + // print the file each element sperated by a \n...\n + for (i, (body, _)) in file.iter().enumerate() { + write!(f, "{}", body)?; + if i != file.len() - 1 { + write!(f, "\n...\n")?; + } + } + Ok(()) + } + } + + impl FileTrait for $name { + fn get_language(&self) -> Language { + Language::$filtername + } + fn get_file_name(&self) -> String { + self.file_name.clone() + } + fn get_functions(&self) -> Vec> { + self.functions + .clone() + .iter() + .cloned() + .map(|x| Box::new(x) as Box) + .collect() + } + fn filter_by(&self, filter: &Filter) -> Result> { + let mut filtered_functions = Vec::new(); + if let Filter::PLFilter(LanguageFilter::$filtername(_)) + | Filter::FunctionInLines(..) = filter + { + } else if matches!(filter, Filter::None) { + return Ok(self.clone()); + } else { + return Err("filter not supported for this type")?; + } + for function in &self.functions { + match filter { + Filter::FunctionInLines(start, end) => { + if function.get_lines().0 >= *start && function.get_lines().1 <= *end { + filtered_functions.push(function.clone()); + } + } + Filter::PLFilter(LanguageFilter::$filtername(filter)) => { + if filter.matches(function) { + filtered_functions.push(function.clone()); + } + } + _ => {} + } + } + Ok($name::new(self.file_name.clone(), filtered_functions)) + } + fn get_current(&self) -> Option> { + self.functions + .get(self.current_pos) + .map(|function| Box::new(function.clone()) as Box) + } + } + + impl $name { + pub fn new(file_name: String, functions: Vec<$function>) -> Self { + $name { + file_name, + functions, + current_pos: 0, + } + } + } + }; +} + +make_file!(PythonFile, PythonFunction, Python); +make_file!(RustFile, RustFunction, Rust); +// #[cfg(feature = "c_lang")] +// make_file!(CFile, CFunction, C); +#[cfg(feature = "unstable")] +make_file!(GoFile, GoFunction, Go); +make_file!(RubyFile, RubyFunction, Ruby); + +#[cfg(test)] +mod lang_tests { + // macro that auto genertes the test parse__file_time + macro_rules! make_file_time_test { + ($name:ident, $extname:ident, $function:ident, $filetype:ident) => { + #[test] + fn $name() { + let mut file = std::env::current_dir().unwrap(); + file.push("src"); + file.push("test_functions.".to_string() + stringify!($extname)); + let files = std::fs::read_to_string(file.clone()) + .expect(format!("could not read file {:?}", file).as_str()); + let start = std::time::Instant::now(); + let ok = $function::find_function_in_file(&files, "empty_test"); + let end = std::time::Instant::now(); + match &ok { + Ok(hist) => { + // turn the hist into a file + let file = $filetype::new(file.display().to_string(), hist.clone()); + println!("{}", file); + println!("-------------------"); + for i in hist { + println!("{}", i); + println!("{:?}", i); + } + } + Err(e) => { + println!("{}", e); + } + } + println!("{} took {:?}", stringify!($name), end - start); + assert!(ok.is_ok()); + } + }; + } + + use super::*; + make_file_time_test!(python_parses, py, python, PythonFile); + make_file_time_test!(rust_parses, rs, rust, RustFile); + // #[cfg(feature = "c_lang")] + // make_file_time_test!(c_parses, c, c, CFile); + #[cfg(feature = "unstable")] + make_file_time_test!(go_parses, go, go, GoFile); + make_file_time_test!(ruby_parses, rb, ruby, RubyFile); +} diff --git a/git-function-history-lib/src/languages/python.rs b/git-function-history-lib/src/languages/python.rs new file mode 100644 index 00000000..a0cebbe0 --- /dev/null +++ b/git-function-history-lib/src/languages/python.rs @@ -0,0 +1,607 @@ +use rustpython_parser::{ + ast::{Arguments, ExprKind, Located, StmtKind}, + parser, +}; +use std::collections::VecDeque; +use std::{collections::HashMap, fmt}; + +use crate::{impl_function_trait, UnwrapToError}; + +use super::FunctionTrait; + +#[derive(Debug, Clone)] +/// A python function +pub struct PythonFunction { + pub(crate) name: String, + pub(crate) body: String, + pub(crate) parameters: PythonParams, + pub(crate) parent: Vec, + pub(crate) decorators: Vec<(usize, String)>, + pub(crate) class: Vec, + pub(crate) lines: (usize, usize), + pub(crate) returns: Option, +} + +impl fmt::Display for PythonFunction { + /// don't use this for anything other than debugging the output is not guaranteed to be in the right order + /// use `fmt::Displa`y for `PythonFile` instead + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for class in &self.class { + for decorator in &class.decorators { + write!(f, "{}\n...\n", decorator.1)?; + } + write!(f, "{}\n...\n", class.top)?; + } + for parent in &self.parent { + for decorator in &parent.decorators { + write!(f, "{}\n...\n", decorator.1)?; + } + write!(f, "{}\n...\n", parent.top)?; + } + for decorator in &self.decorators { + write!(f, "{}\n...\n", decorator.1)?; + } + write!(f, "{}", self.body) + } +} + +#[derive(Debug, Clone)] +/// A single parameter of a python function +pub struct Param { + /// The name of the parameter + pub name: String, + /// The optional type of the parameter + pub r#type: Option, +} + +#[derive(Debug, Clone)] +/// The parameters of a python function +/// refer to python docs for more info +/// note: currently we don't save default values +pub struct PythonParams { + pub args: Vec, + pub kwargs: Vec, + pub posonlyargs: Vec, + pub varargs: Option, + pub varkwargs: Option, +} + +impl PythonParams { + pub fn arg_has_name(&self, name: &str) -> bool { + self.args.iter().any(|arg| arg.name == name) + || self.kwargs.iter().any(|arg| arg.name == name) + || self.posonlyargs.iter().any(|arg| arg.name == name) + || self.varargs.as_ref().map_or(false, |arg| arg.name == name) + || self + .varkwargs + .as_ref() + .map_or(false, |arg| arg.name == name) + } +} + +#[derive(Debug, Clone)] +/// A python class +pub struct PythonClass { + pub(crate) name: String, + pub(crate) top: String, + pub(crate) lines: (usize, usize), + pub(crate) decorators: Vec<(usize, String)>, +} +#[derive(Debug, Clone)] +/// A python function that is a parent of another python function, we don't keep the body of the function +pub struct PythonParentFunction { + pub(crate) name: String, + pub(crate) top: String, + pub(crate) lines: (usize, usize), + pub(crate) parameters: PythonParams, + pub(crate) decorators: Vec<(usize, String)>, + pub(crate) returns: Option, +} + +pub(crate) fn find_function_in_file( + file_contents: &str, + name: &str, +) -> Result, Box> { + let ast = parser::parse_program(file_contents, "")?; + let mut functions = vec![]; + if ast.is_empty() { + return Err("No code found")?; + } + let mut starts = file_contents + .match_indices('\n') + .map(|x| x.0) + .collect::>(); + starts.push(0); + starts.sort_unstable(); + let map = starts + .iter() + .enumerate() + .collect::>(); + get_functions_recurisve( + ast, + &map, + &mut functions, + &mut Vec::new(), + &mut Vec::new(), + name, + ); + let mut new = Vec::new(); + for func in functions { + let start = func.0.location.row(); + let end = func + .0 + .end_location + .unwrap_to_error("no end location for this function")? + .row(); + let (Some(starts), Some(ends)) = (map.get(&(start - 1)), map.get(&(end))) else { continue }; + if let StmtKind::FunctionDef { + name, + args, + decorator_list, + returns, + .. + } + | StmtKind::AsyncFunctionDef { + name, + args, + decorator_list, + returns, + .. + } = func.0.node + { + let start_line = func.0.location.row(); + let body = match file_contents.get(**starts..**ends) { + Some(str) => str, + None => continue, + } + .trim_start_matches('\n'); + let body = super::make_lined(body, start_line); + let class = func + .1 + .iter() + .filter_map(|class| { + if let StmtKind::ClassDef { + ref name, + ref decorator_list, + .. + } = class.node + { + let start = class.location.row(); + if let Some(end) = class.end_location { + let end = end.row(); + let top = match file_contents.lines().nth(start - 1) { + Some(l) => l.trim_end().to_string(), + None => return None, + }; + let top = format!("{start}: {top}"); + let decorators = get_decorator_list_new(decorator_list, file_contents); + return Some(PythonClass { + name: name.to_string(), + top, + lines: (start, end), + decorators, + }); + } + } + None + }) + .collect::>(); + let parent = func + .2 + .iter() + .filter_map(|parent| { + if let StmtKind::FunctionDef { + ref name, + ref args, + ref decorator_list, + ref returns, + .. + } + | StmtKind::AsyncFunctionDef { + ref name, + ref args, + ref decorator_list, + ref returns, + .. + } = parent.node + { + let start = parent.location.row(); + if let Some(end) = parent.end_location { + let end = end.row(); + let top = match file_contents.lines().nth(start - 1) { + Some(l) => l.trim_end().to_string(), + None => return None, + }; + let top = format!("{start}: {top}"); + let decorators = get_decorator_list_new(decorator_list, file_contents); + let parameters = get_args(*args.clone()); + let returns = get_return_type(returns.clone()); + return Some(PythonParentFunction { + name: name.to_string(), + top, + lines: (start, end), + parameters, + decorators, + returns, + }); + } + } + None + }) + .collect::>(); + + let new_func = PythonFunction { + name: name.to_string(), + parameters: get_args(*args), + parent, + decorators: get_decorator_list_new(&decorator_list, file_contents), + returns: get_return_type(returns), + class, + body, + lines: (start, end), + }; + new.push(new_func); + } + } + if new.is_empty() { + Err("No function found")?; + } + Ok(new) +} +#[inline] +fn get_functions_recurisve( + body: Vec>, + map: &HashMap, + functions: &mut Vec<( + Located, + Vec>, + Vec>, + )>, + current_parent: &mut Vec>, + current_class: &mut Vec>, + lookup_name: &str, +) { + let mut new_ast = VecDeque::from(body); + loop { + if new_ast.is_empty() { + break; + } + let stmt = new_ast.pop_front().expect("No stmt found edge case shouldn't happen please file a bug to https://github.com/mendelsshop/git_function_history/issues"); + get_functions( + stmt, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + } +} + +fn get_functions( + stmt: Located, + map: &HashMap, + functions: &mut Vec<( + Located, + Vec>, + Vec>, + )>, + current_parent: &mut Vec>, + current_class: &mut Vec>, + lookup_name: &str, +) { + let stmt_clone = stmt.clone(); + match stmt.node { + StmtKind::FunctionDef { ref name, .. } | StmtKind::AsyncFunctionDef { ref name, .. } + if name == lookup_name => + { + if stmt.end_location.is_some() { + functions.push((stmt, current_class.clone(), current_parent.clone())); + } + } + StmtKind::If { body, orelse, .. } + | StmtKind::While { body, orelse, .. } + | StmtKind::For { body, orelse, .. } + | StmtKind::AsyncFor { body, orelse, .. } => { + get_functions_recurisve( + body, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + get_functions_recurisve( + orelse, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + } + StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => { + current_parent.push(stmt_clone); + get_functions_recurisve( + body, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + current_parent.pop(); + } + StmtKind::ClassDef { body, .. } => { + current_class.push(stmt_clone); + get_functions_recurisve( + body, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + current_class.pop(); + } + StmtKind::With { body, .. } | StmtKind::AsyncWith { body, .. } => get_functions_recurisve( + body, + map, + functions, + current_parent, + current_class, + lookup_name, + ), + StmtKind::Try { + body, + orelse, + finalbody, + .. + } => { + get_functions_recurisve( + body, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + get_functions_recurisve( + orelse, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + get_functions_recurisve( + finalbody, + map, + functions, + current_parent, + current_class, + lookup_name, + ); + } + _ => {} + } +} +// TODO save arg.defaults & arg.kwdefaults and attempt to map them to the write parameters +fn get_args(args: Arguments) -> PythonParams { + let mut parameters = PythonParams { + args: Vec::new(), + varargs: None, + posonlyargs: Vec::new(), + kwargs: Vec::new(), + varkwargs: None, + }; + for arg in args.args { + parameters.args.push(Param { + name: arg.node.arg, + r#type: arg.node.annotation.and_then(|x| { + if let ExprKind::Name { id, .. } = x.node { + Some(id) + } else { + None + } + }), + }); + } + for arg in args.kwonlyargs { + parameters.kwargs.push(Param { + name: arg.node.arg, + r#type: arg.node.annotation.and_then(|x| { + if let ExprKind::Name { id, .. } = x.node { + Some(id) + } else { + None + } + }), + }); + } + if let Some(arg) = args.vararg { + parameters.varargs = Some(Param { + name: arg.node.arg, + r#type: arg.node.annotation.and_then(|x| { + if let ExprKind::Name { id, .. } = x.node { + Some(id) + } else { + None + } + }), + }); + } + if let Some(arg) = args.kwarg { + parameters.varkwargs = Some(Param { + name: arg.node.arg, + r#type: arg.node.annotation.and_then(|x| { + if let ExprKind::Name { id, .. } = x.node { + Some(id) + } else { + None + } + }), + }); + } + for arg in args.posonlyargs { + parameters.posonlyargs.push(Param { + name: arg.node.arg, + r#type: arg.node.annotation.and_then(|x| { + if let ExprKind::Name { id, .. } = x.node { + Some(id) + } else { + None + } + }), + }); + } + + parameters +} + +fn get_return_type(retr: Option>>) -> Option { + if let Some(retr) = retr { + if let ExprKind::Name { ref id, .. } = retr.node { + return Some(id.to_string()); + } + } + None +} +#[allow(dead_code)] +// keeping this here just in case +fn get_decorator_list(decorator_list: &[Located]) -> Vec<(usize, String)> { + decorator_list + .iter() + .map(located_expr_to_decorator) + .collect::>() +} + +fn located_expr_to_decorator(expr: &Located) -> (usize, String) { + ( + expr.location.row(), + format!( + "{}:{}decorator with {}", + expr.location.row(), + vec![" "; expr.location.column()].join(""), + expr.node.name() + ), + ) +} + +fn get_located_expr_line(expr: &Located, file_contents: &str) -> Option { + // does not add line numbers to string + file_contents + .lines() + .nth(expr.location.row() - 1) + .map(ToString::to_string) +} + +fn get_decorator_list_new( + decorator_list: &[Located], + file_contents: &str, +) -> Vec<(usize, String)> { + decorator_list + .iter() + .map(|x| { + get_located_expr_line(x, file_contents).map_or_else( + || located_expr_to_decorator(x), + |dec| (x.location.row(), super::make_lined(&dec, x.location.row())), + ) + }) + .collect::>() +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PythonFilter { + /// when you want to filter by function that are in a specific class + InClass(String), + /// when you want filter by a function that has a parent function of a specific name + HasParentFunction(String), + /// when you want to filter by a function that has a has a specific return type + HasReturnType(String), + /// when you want to filter by a function that has a specific parameter name + HasParameterName(String), + /// when you want to filter by a function that has a specific decorator + HasDecorator(String), + /// when you want to filter by a function thats class has a specific decorator + HasClasswithDecorator(String), + /// when you want to filter by a function that's parent function has a specific decorator + HasParentFunctionwithDecorator(String), + /// when you want to filter by a function that's parent function has a specific parameter name + HasParentFunctionwithParameterName(String), + /// when you want to filter by a function that's parent function has a specific return type + HasParentFunctionwithReturnType(String), +} + +impl PythonFilter { + pub fn matches(&self, function: &PythonFunction) -> bool { + match self { + Self::InClass(class) => function.class.iter().any(|c| c.name == *class), + Self::HasParentFunction(parent) => function.parent.iter().any(|x| x.name == *parent), + Self::HasReturnType(return_type) => function + .returns + .as_ref() + .map_or(false, |x| x == return_type), + Self::HasParameterName(parameter_name) => { + function.parameters.arg_has_name(parameter_name) + } + Self::HasDecorator(decorator) => { + function.decorators.iter().any(|x| x.1.contains(decorator)) + } + Self::HasClasswithDecorator(decorator) => function + .class + .iter() + .any(|x| x.decorators.iter().any(|y| y.1.contains(decorator))), + Self::HasParentFunctionwithDecorator(decorator) => function + .parent + .iter() + .any(|x| x.decorators.iter().any(|x| x.1.contains(decorator))), + Self::HasParentFunctionwithParameterName(parameter_name) => function + .parent + .iter() + .any(|x| x.parameters.arg_has_name(parameter_name)), + Self::HasParentFunctionwithReturnType(return_type) => function + .parent + .iter() + .any(|x| x.returns.as_ref().map_or(false, |x| x == return_type)), + } + } +} + +impl FunctionTrait for PythonFunction { + fn get_tops(&self) -> Vec<(String, usize)> { + let mut tops = Vec::new(); + for class in &self.class { + tops.push((class.top.clone(), class.lines.0)); + for decorator in &class.decorators { + tops.push((decorator.1.clone(), decorator.0)); + } + } + for decorator in &self.decorators { + tops.push((decorator.1.clone(), decorator.0)); + } + for parent in &self.parent { + tops.push((parent.top.clone(), parent.lines.0)); + for decorator in &parent.decorators { + tops.push((decorator.1.clone(), decorator.0)); + } + } + tops.sort_by(|top1, top2| top1.1.cmp(&top2.1)); + tops + } + + fn get_bottoms(&self) -> Vec<(String, usize)> { + Vec::new() + } + + fn get_total_lines(&self) -> (usize, usize) { + // find the first line of the function (could be the parent or the class) + self.class + .iter() + .map(|x| x.lines) + .chain(self.parent.iter().map(|x| x.lines)) + .min() + .unwrap_or(self.lines) + } + impl_function_trait!(PythonFunction); +} diff --git a/git-function-history-lib/src/languages/ruby.rs b/git-function-history-lib/src/languages/ruby.rs new file mode 100644 index 00000000..3e809d55 --- /dev/null +++ b/git-function-history-lib/src/languages/ruby.rs @@ -0,0 +1,341 @@ +use std::{collections::HashMap, error::Error, fmt}; + +use lib_ruby_parser::{ + nodes::{Class, Def}, + source::DecodedInput, + Loc, Parser, ParserOptions, +}; + +use crate::UnwrapToError; + +use super::FunctionTrait; + +#[derive(Debug, Clone, PartialEq, Eq)] +// repersentation of a ruby function +pub struct RubyFunction { + pub name: String, + pub lines: (usize, usize), + pub class: Vec, + pub args: RubyParams, + pub body: String, +} + +impl RubyFunction { + pub const fn new( + name: String, + lines: (usize, usize), + class: Vec, + args: RubyParams, + body: String, + ) -> Self { + Self { + name, + lines, + class, + args, + body, + } + } +} + +impl fmt::Display for RubyFunction { + /// don't use this for anything other than debugging the output is not guaranteed to be in the right order + /// use `fmt::Display` for `RubyFile` instead + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for class in &self.class { + write!(f, "{}", class.top)?; + } + write!(f, "{}", self.body)?; + for class in &self.class { + write!(f, "{}", class.bottom)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// represents a Ruby class +pub struct RubyClass { + pub name: String, + pub lines: (usize, usize), + pub superclass: Option, + pub top: String, + pub bottom: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// repersents parameters of a ruby function +pub struct RubyParams { + /// required parameters + args: Vec, + /// keyword parameters + kwargs: Vec, + /// keyword parameter is nil ie `**nil` in `def foo(a, **nil)` + kwnilarg: bool, + /// parameters are forwarded ie `...` in `def foo(...)` + forwarded_args: bool, + /// parameters that have optional default values ie `a: 1` in `def foo(a: 1)` + kwoptargs: Vec<(String, String)>, + /// keyword rest parameter ie `**a` in `def foo(**a)` + kwrestarg: Option, + /// rest parameter ie `*a` in `def foo(*a)` + restarg: Option, +} + +impl RubyParams { + pub const fn new() -> Self { + Self { + args: Vec::new(), + kwnilarg: false, + forwarded_args: false, + kwrestarg: None, + restarg: None, + kwargs: Vec::new(), + kwoptargs: Vec::new(), + } + } + + fn contains(&self, name: &String) -> bool { + self.args.contains(name) + || self.kwargs.contains(name) + || self + .kwoptargs + .iter() + .any(|(arg, default)| arg == name || default == name) + || Some(name.clone()) == self.kwrestarg + || Some(name.clone()) == self.restarg + } +} +impl Default for RubyParams { + fn default() -> Self { + Self::new() + } +} + +pub(crate) fn find_function_in_file( + file_contents: &str, + name: &str, +) -> Result, Box> { + let mut starts = file_contents + .match_indices('\n') + .map(|x| x.0) + .collect::>(); + starts.push(0); + starts.sort_unstable(); + let map = starts + .iter() + .enumerate() + .collect::>(); + let parser = Parser::new(file_contents, ParserOptions::default()); + let parsed = parser.do_parse(); + // POSSBLE TODO check if there is any error dianostics parsed.dadnostices and return error is so + let ast = parsed.ast.unwrap_to_error("Failed to parse file")?; + let fns = get_functions_from_node(&ast, &vec![], name); + let index = super::turn_into_index(file_contents)?; + let fns = fns + .iter() + .map(|(f, c)| { + let class = c + .iter() + .filter_map(|c| { + let start_line = super::get_from_index(&index, c.expression_l.begin)?; + let end_line = super::get_from_index(&index, c.expression_l.end)?; + let loc_end = c.end_l; + let top = Loc { + begin: **map.get(&(start_line - 1))?, + end: c.body.as_ref().map_or( + c.superclass + .as_ref() + .map_or(c.name.expression().end, |c| c.expression().end), + |b| b.expression().begin, + ) - 1, + }; + let mut top = top.source(&parsed.input)?; + top = top.trim_matches('\n').to_string(); + top = super::make_lined(&top, start_line); + Some(RubyClass { + name: parser_class_name(c), + lines: (start_line, end_line), + superclass: parse_superclass(c), + top, + bottom: super::make_lined( + loc_end + .with_begin(**map.get(&(end_line - 1))?) + .source(&parsed.input)? + .trim_matches('\n'), + end_line, + ), + }) + }) + .collect(); + let start = f.expression_l.begin; + // get the lines from map using f.expression_l.begin and f.expression_l.end + let start_line = super::get_from_index(&index, start) + .unwrap_to_error("Failed to get start lines")?; + let end_line = super::get_from_index(&index, f.expression_l.end) + .unwrap_to_error("Failed to get end line")?; + let starts = start_line + 1; + Ok::>(RubyFunction { + name: f.name.clone(), + lines: (start_line, end_line), + class, + body: super::make_lined( + f.expression_l + .with_begin(**map.get(&(start_line - 1)).unwrap_or(&&0)) + .source(&parsed.input) + .unwrap_to_error("Failed to get function body from source")? + .trim_matches('\n'), + starts - 1, + ), + args: f + .args + .clone() + .map_or_else(RubyParams::new, |a| parse_args_from_node(&a, &parsed.input)), + }) + }) + .filter_map(Result::ok) + .collect::>(); + if fns.is_empty() { + Err("No functions with this name was found in the this file")?; + } + Ok(fns) +} + +fn get_functions_from_node( + node: &lib_ruby_parser::Node, + class: &Vec, + name: &str, +) -> Vec<(Def, Vec)> { + match node { + lib_ruby_parser::Node::Def(def) => { + if def.name == name { + vec![(def.clone(), class.clone())] + } else { + vec![] + } + } + lib_ruby_parser::Node::Class(new_class) => { + let mut functions = vec![]; + let mut new_list = class.clone(); + new_list.push(new_class.clone()); + for child in &new_class.body { + functions.extend(get_functions_from_node(child, &new_list, name)); + } + functions + } + lib_ruby_parser::Node::Begin(stat) => { + let mut functions = vec![]; + for child in &stat.statements { + functions.extend(get_functions_from_node(child, class, name)); + } + functions + } + _ => vec![], + } +} + +fn parse_args_from_node(node: &lib_ruby_parser::Node, parsed_file: &DecodedInput) -> RubyParams { + let mut ret_args = RubyParams::new(); + if let lib_ruby_parser::Node::Args(args) = node { + args.args.iter().for_each(|arg| match arg { + // basic arg + lib_ruby_parser::Node::Arg(arg) => ret_args.args.push(arg.name.clone()), + lib_ruby_parser::Node::Kwarg(arg) => ret_args.kwargs.push(arg.name.clone()), + // args that has a default value + lib_ruby_parser::Node::Kwoptarg(arg) => { + ret_args.kwoptargs.push(( + arg.name.clone(), + arg.default + .expression() + .source(parsed_file) + .unwrap_or_else(|| "could not retrieve default value".to_string()), + )); + } + lib_ruby_parser::Node::Restarg(arg) => { + if let Some(name) = &arg.name { + ret_args.restarg = Some(name.clone()); + } + } + lib_ruby_parser::Node::Kwrestarg(arg) => { + if let Some(name) = &arg.name { + ret_args.kwrestarg = Some(name.clone()); + } + } + + // ruby specific + lib_ruby_parser::Node::ForwardArg(_) => ret_args.forwarded_args = true, + lib_ruby_parser::Node::Kwnilarg(_) => ret_args.kwnilarg = true, + // Node::ForwardedArgs and Node::Kwargs, node::Optarg are for method calls and not definitions thus we are not matching on them + // Node::Blockarg is for block methods similar to labmdas whic we do not currently support + _ => {} + }); + }; + ret_args +} + +fn parser_class_name(class: &Class) -> String { + match class.name.as_ref() { + lib_ruby_parser::Node::Const(constant) => constant.name.clone(), + _ => String::new(), + } +} + +fn parse_superclass(class: &Class) -> Option { + class + .superclass + .as_ref() + .and_then(|superclass| match superclass.as_ref() { + lib_ruby_parser::Node::Const(constant) => Some(constant.name.clone()), + _ => None, + }) +} + +impl FunctionTrait for RubyFunction { + crate::impl_function_trait!(RubyFunction); + + fn get_total_lines(&self) -> (usize, usize) { + self.class + .iter() + .map(|class| class.lines) + .min_by(Ord::cmp) + .unwrap_or(self.lines) + } + + fn get_tops(&self) -> Vec<(String, usize)> { + self.class + .iter() + .map(|class| (class.top.clone(), class.lines.0)) + .collect() + } + + fn get_bottoms(&self) -> Vec<(String, usize)> { + self.class + .iter() + .map(|class| (class.bottom.clone(), class.lines.1)) + .collect() + } +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RubyFilter { + /// find a Ruby functions in a specific class + FunctionInClass(String), + /// find a Ruby function with a specific parameter + FunctionWithParameter(String), + /// find a Ruby function in a class that inherits from a specific class + FunctionWithSuperClass(String), +} + +impl RubyFilter { + pub fn matches(&self, function: &RubyFunction) -> bool { + match self { + Self::FunctionInClass(name) => function.class.iter().any(|class| &class.name == name), + Self::FunctionWithParameter(name) => function.args.contains(name), + Self::FunctionWithSuperClass(name) => function.class.iter().any(|class| { + class + .superclass + .as_ref() + .map_or(false, |superclass| superclass == name) + }), + } + } +} diff --git a/git-function-history-lib/src/languages/rust.rs b/git-function-history-lib/src/languages/rust.rs new file mode 100644 index 00000000..7070956e --- /dev/null +++ b/git-function-history-lib/src/languages/rust.rs @@ -0,0 +1,614 @@ +use std::{collections::HashMap, error::Error, fmt}; + +use ra_ap_syntax::{ + ast::{self, HasDocComments, HasGenericParams, HasName}, + AstNode, SourceFile, SyntaxKind, +}; + +use crate::{impl_function_trait, UnwrapToError}; + +use super::FunctionTrait; + +/// This holds the information about a single function each commit will have multiple of these. +#[derive(Debug, Clone)] +pub struct RustFunction { + pub(crate) name: String, + /// The actual code of the function + pub(crate) body: String, + /// is the function in a block ie `impl` `trait` etc + pub(crate) block: Option, + /// optional parent functions + pub(crate) function: Vec, + /// The line number the function starts and ends on + pub(crate) lines: (usize, usize), + /// The lifetime of the function + pub(crate) lifetime: Vec, + /// The generic types of the function + /// also includes lifetimes + pub(crate) generics: Vec, + /// The arguments of the function + pub(crate) arguments: HashMap, + /// The return type of the function + pub(crate) return_type: Option, + /// The functions atrributes + pub(crate) attributes: Vec, + /// the functions doc comments + pub(crate) doc_comments: Vec, +} + +impl RustFunction { + /// get the parent functions + pub fn get_parent_function(&self) -> Vec { + self.function.clone() + } + + /// get the block of the function + pub fn get_block(&self) -> Option { + self.block.clone() + } +} + +impl fmt::Display for RustFunction { + /// don't use this for anything other than debugging the output is not guaranteed to be in the right order + /// use `fmt::Display` for `RustFile` instead + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.block { + None => {} + Some(block) => write!(f, "{}\n...\n", block.top)?, + }; + for i in &self.function { + write!(f, "{}\n...\n", i.top)?; + } + write!(f, "{}", self.body)?; + for i in &self.function { + write!(f, "\n...\n{}", i.bottom)?; + } + match &self.block { + None => {} + Some(block) => write!(f, "\n...\n{}", block.bottom)?, + }; + Ok(()) + } +} + +/// This is used for the functions that are being looked up themeselves but store an outer function that may aontains a function that is being looked up. +#[derive(Debug, Clone)] +pub struct RustParentFunction { + /// The name of the function (parent function) + pub(crate) name: String, + /// what the signature of the function is + pub(crate) top: String, + /// what the last line of the function is + pub(crate) bottom: String, + /// The line number the function starts and ends on + pub(crate) lines: (usize, usize), + /// The lifetime of the function + pub(crate) lifetime: Vec, + /// The generic types of the function + /// also includes lifetimes + pub(crate) generics: Vec, + /// The arguments of the function + pub(crate) arguments: HashMap, + /// The return type of the function + pub(crate) return_type: Option, + /// the function atrributes + pub(crate) attributes: Vec, + /// the functions doc comments + pub(crate) doc_comments: Vec, +} + +impl RustParentFunction { + /// get the metadata for this block ie the name of the block, the type of block, the line number the block starts and ends + pub fn get_metadata(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert("name".to_string(), self.name.clone()); + map.insert("lines".to_string(), format!("{:?}", self.lines)); + map.insert("signature".to_string(), self.top.clone()); + map.insert("bottom".to_string(), self.bottom.clone()); + map.insert("generics".to_string(), self.generics.join(",")); + map.insert( + "arguments".to_string(), + self.arguments + .iter() + .map(|(k, v)| format!("{k}: {v}")) + .collect::>() + .join(","), + ); + map.insert("lifetime generics".to_string(), self.lifetime.join(",")); + map.insert("attributes".to_string(), self.attributes.join(",")); + map.insert("doc comments".to_string(), self.doc_comments.join(",")); + self.return_type.as_ref().map_or((), |return_type| { + map.insert("return type".to_string(), return_type.clone()); + }); + map + } +} + +/// This holds information about when a function is in an impl/trait/extern block +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Block { + /// The name of the block ie for `impl` it would be the type were impling for + pub(crate) name: Option, + /// The signature of the block + pub(crate) top: String, + /// The last line of the block + pub(crate) bottom: String, + /// the type of block ie `impl` `trait` `extern` + pub(crate) block_type: BlockType, + /// The line number the function starts and ends on + pub(crate) lines: (usize, usize), + /// The lifetime of the function + pub(crate) lifetime: Vec, + /// The generic types of the function + /// also includes lifetimes + pub(crate) generics: Vec, + /// The blocks atrributes + pub(crate) attributes: Vec, + /// the blocks doc comments + pub(crate) doc_comments: Vec, +} + +impl Block { + /// get the metadata for this block ie the name of the block, the type of block, the line number the block starts and ends + pub fn get_metadata(&self) -> HashMap { + let mut map = HashMap::new(); + if let Some(name) = &self.name { + map.insert("name".to_string(), name.to_string()); + } + map.insert("block".to_string(), format!("{}", self.block_type)); + map.insert("lines".to_string(), format!("{:?}", self.lines)); + map.insert("signature".to_string(), self.top.clone()); + map.insert("bottom".to_string(), self.bottom.clone()); + map.insert("generics".to_string(), self.generics.join(",")); + map.insert("lifetime generics".to_string(), self.lifetime.join(",")); + map.insert("attributes".to_string(), self.attributes.join(",")); + map.insert("doc comments".to_string(), self.doc_comments.join(",")); + map + } +} + +/// This enum is used when filtering commit history only for let say impl and not externs or traits +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum BlockType { + /// This is for `impl` blocks + Impl, + /// This is for `trait` blocks + Extern, + /// This is for `extern` blocks + Trait, + /// This is for code that gets labeled as a block but `get_function_history` can't find a block type + Unknown, +} + +impl BlockType { + /// This is used to get the name of the block type from a string + pub fn from_string(s: &str) -> Self { + match s { + "impl" => Self::Impl, + "extern" => Self::Extern, + "trait" => Self::Trait, + _ => Self::Unknown, + } + } +} + +impl fmt::Display for BlockType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Impl => write!(f, "impl"), + Self::Extern => write!(f, "extern"), + Self::Trait => write!(f, "trait"), + Self::Unknown => write!(f, "unknown"), + } + } +} + +// TODO: split this function into smaller functions +pub(crate) fn find_function_in_file( + file_contents: &str, + name: &str, +) -> Result, Box> { + let mut functions = Vec::new(); + get_function_asts(name, file_contents, &mut functions); + let mut starts = file_contents + .match_indices('\n') + .map(|x| x.0) + .collect::>(); + starts.push(0); + starts.sort_unstable(); + let map = starts + .iter() + .enumerate() + .collect::>(); + let mut hist = Vec::new(); + for f in &functions { + let stuff = get_stuff(f, file_contents, &map).unwrap_to_error("could not get endline")?; + let generics = get_genrerics_and_lifetime(f); + let mut parent = f.syntax().parent(); + let mut parent_fn: Vec = Vec::new(); + let mut parent_block = None; + while let Some(p) = parent.into_iter().next() { + if p.kind() == SyntaxKind::SOURCE_FILE { + break; + } + ast::Fn::cast(p.clone()).map_or_else( + || { + if let Some(block) = ast::Impl::cast(p.clone()) { + let attr = get_doc_comments_and_attrs(&block); + let Some(stuff) = get_stuff(&block, file_contents, &map) else { + return; + }; + let generics = get_genrerics_and_lifetime(&block); + parent_block = Some(Block { + name: block.self_ty().map(|ty| ty.to_string()), + lifetime: generics.1, + generics: generics.0, + top: stuff.1 .0, + bottom: stuff.1 .1, + block_type: BlockType::Impl, + lines: (stuff.0 .0, stuff.0 .1), + attributes: attr.1, + doc_comments: attr.0, + }); + } else if let Some(block) = ast::Trait::cast(p.clone()) { + let attr = get_doc_comments_and_attrs(&block); + let Some(stuff) = get_stuff(&block, file_contents, &map) else { + return; + }; + let generics = get_genrerics_and_lifetime(&block); + parent_block = Some(Block { + name: block.name().map(|ty| ty.to_string()), + lifetime: generics.1, + generics: generics.0, + top: stuff.1 .0, + bottom: stuff.1 .1, + block_type: BlockType::Trait, + lines: (stuff.0 .0, stuff.0 .1), + attributes: attr.1, + doc_comments: attr.0, + }); + } else if let Some(block) = ast::ExternBlock::cast(p.clone()) { + let attr = get_doc_comments_and_attrs(&block); + let stuff = get_stuff(&block, file_contents, &map); + if let Some(stuff) = stuff { + parent_block = Some(Block { + name: None, + lifetime: Vec::new(), + generics: Vec::new(), + top: stuff.1 .0, + bottom: stuff.1 .1, + block_type: BlockType::Extern, + lines: (stuff.0 .0, stuff.0 .1), + attributes: attr.1, + doc_comments: attr.0, + }); + } + } + }, + |function: ast::Fn| { + let Some(stuff) = get_stuff(&function, file_contents, &map) else { + return; + }; + let generics = get_genrerics_and_lifetime(&function); + let attr = get_doc_comments_and_attrs(&function); + let name = match function.name() { + Some(name) => name.to_string(), + None => return, + }; + parent_fn.push(RustParentFunction { + name, + lifetime: generics.1, + generics: generics.0, + top: stuff.1 .0, + bottom: stuff.1 .1, + lines: (stuff.0 .0, stuff.0 .1), + return_type: function.ret_type().map(|ty| ty.to_string()), + arguments: f.param_list().map_or_else(HashMap::new, |args| { + args.params() + .filter_map(|arg| { + arg.to_string() + .rsplit_once(": ") + .map(|x| (x.0.to_string(), x.1.to_string())) + }) + .collect::>() + }), + attributes: attr.1, + doc_comments: attr.0, + }); + }, + ); + parent = p.parent(); + } + let attr = get_doc_comments_and_attrs(f); + let start_line = stuff.0 .0; + let start_idx = match map + .get(&(start_line - 1)) + .unwrap_to_error("could not get start index for function based off line number")? + { + 0 => 0, + x => *x + 1, + }; + let contents: String = file_contents + .get(start_idx..f.syntax().text_range().end().into()) + .unwrap_to_error("could not function text based off of start and stop indexes")? + .to_string(); + let body = super::make_lined(&contents, start_line); + let function = RustFunction { + name: match f.name() { + Some(name) => name.to_string(), + None => continue, + }, + body, + block: parent_block, + function: parent_fn, + return_type: f.ret_type().map(|ty| ty.to_string()), + arguments: f.param_list().map_or_else(HashMap::new, |args| { + args.params() + .filter_map(|arg| { + arg.to_string() + .rsplit_once(": ") + .map(|x| (x.0.to_string(), x.1.to_string())) + }) + .collect::>() + }), + lifetime: generics.1, + generics: generics.0, + lines: (stuff.0 .0, stuff.0 .1), + attributes: attr.1, + doc_comments: attr.0, + }; + hist.push(function); + } + if hist.is_empty() { + Err("no function found")?; + } + Ok(hist) +} +#[inline] +fn get_function_asts(name: &str, file: &str, functions: &mut Vec) { + let parsed_file = SourceFile::parse(file).tree(); + parsed_file + .syntax() + .descendants() + .filter_map(ast::Fn::cast) + .filter(|function| function.name().map_or(false, |n| n.text() == name)) + .for_each(|function| functions.push(function)); +} +#[inline] +fn get_stuff( + block: &T, + file: &str, + map: &HashMap, +) -> Option<((usize, usize), (String, String))> { + let start = block.syntax().text_range().start(); + let end: usize = block.syntax().text_range().end().into(); + // get the start and end lines + let mut found_start_brace = 0; + let index = super::turn_into_index(file).ok()?; + let end_line = super::get_from_index(&index, end - 1)?; + let start_line = super::get_from_index(&index, start.into())?; + for (i, line) in file.chars().enumerate() { + if line == '{' && found_start_brace == 0 && usize::from(start) < i { + found_start_brace = i; + break; + } + } + if found_start_brace == 0 { + found_start_brace = usize::from(start); + } + let start_idx = map.get(&(start_line - 1))?; + let start_lines = start_line; + let mut content: String = file.get((**start_idx)..=found_start_brace)?.to_string(); + if content.get(..1)? == "\n" { + content = content.get(1..)?.to_string(); + } + Some(( + (start_line, end_line), + ( + super::make_lined(&content, start_lines), + super::make_lined(file.lines().nth(end_line - 1).unwrap_or("}"), end_line), + ), + )) +} +#[inline] +fn get_genrerics_and_lifetime(block: &T) -> (Vec, Vec) { + // TODO: map trait bounds from where clauses to the generics so it will return a (HashMap>, HashMap>) + // the key of each hashmap will be the name of the generic/lifetime and the values will be the trait bounds + // and also use type_or_const_params + block.generic_param_list().map_or_else( + || (vec![], vec![]), + |gt| { + ( + // gt. + gt.generic_params() + .map(|gt| gt.to_string()) + .collect::>(), + gt.lifetime_params() + .map(|lt| lt.to_string()) + .collect::>(), + ) + }, + ) +} +#[inline] +fn get_doc_comments_and_attrs(block: &T) -> (Vec, Vec) { + ( + block + .doc_comments() + .map(|c| c.to_string()) + .collect::>(), + block + .attrs() + .map(|c| c.to_string()) + .collect::>(), + ) +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RustFilter { + /// when you want to filter by function that are in a specific block (impl, trait, extern) + InBlock(BlockType), + /// when you want filter by a function that has a parent function of a specific name + HasParentFunction(String), + /// when you want to filter by a function that has a has a specific return type + HasReturnType(String), + /// when you want to filter by a function that has a specific parameter type + HasParameterType(String), + /// when you want to filter by a function that has a specific parameter name + HasParameterName(String), + /// when you want to filter by a function that has a specific lifetime + HasLifetime(String), + /// when you want to filter by a function that has a specific generic with name + HasGeneric(String), + /// when you want to filter by a function that has a specific attribute + HasAttribute(String), + /// when you want to filter by a function that has or contains a specific doc comment + HasDocComment(String), + /// when you want to filter by a function that's block has a specific attribute + BlockHasAttribute(String), + /// when you want to filter by a function that's block has a specific doc comment + BlockHasDocComment(String), + /// when you want to filter by a function that's block has a specific lifetime + BlockHasLifetime(String), + /// when you want to filter by a function that's block has a specific generic with name + BlockHasGeneric(String), + /// when you want to filter by a function that's parent function has a specific attribute + ParentFunctionHasAttribute(String), + /// when you want to filter by a function that's parent function has a specific doc comment + ParentFunctionHasDocComment(String), + /// when you want to filter by a function that's parent function has a specific lifetime + ParentFunctionHasLifetime(String), + /// when you want to filter by a function that's parent function has a specific generic with name + ParentFunctionHasGeneric(String), + /// when you want to filter by a function that's parent function has a specific return type + ParentFunctionHasReturnType(String), + /// when you want to filter by a function that's parent function has a specific parameter type + ParentFunctionHasParameterType(String), + /// when you want to filter by a function that's parent function has a specific parameter name + ParentFunctionHasParameterName(String), +} + +impl RustFilter { + pub fn matches(&self, function: &RustFunction) -> bool { + match self { + Self::InBlock(block_type) => function + .block + .as_ref() + .map_or(false, |block| block.block_type == *block_type), + Self::HasParentFunction(parent) => function.function.iter().any(|f| f.name == *parent), + Self::HasReturnType(return_type) => { + function.return_type == Some(return_type.to_string()) + } + Self::HasParameterType(parameter_type) => { + function.arguments.values().any(|x| x == parameter_type) + } + Self::HasParameterName(parameter_name) => { + function.arguments.keys().any(|x| x == parameter_name) + } + Self::HasLifetime(lifetime) => function.lifetime.contains(lifetime), + Self::HasGeneric(generic) => function.generics.contains(generic), + Self::HasAttribute(attribute) => function.attributes.contains(attribute), + Self::HasDocComment(comment) => { + function + .doc_comments + .iter() + .filter(|doc| comment.contains(*doc)) + .count() + > 0 + } + Self::BlockHasAttribute(attribute) => function + .block + .as_ref() + .map_or(false, |block| block.attributes.contains(attribute)), + Self::BlockHasDocComment(comment) => function.block.as_ref().map_or(false, |block| { + block + .doc_comments + .iter() + .filter(|doc| comment.contains(*doc)) + .count() + > 0 + }), + Self::BlockHasLifetime(lifetime) => function + .block + .as_ref() + .map_or(false, |block| block.lifetime.contains(lifetime)), + Self::BlockHasGeneric(generic) => function + .block + .as_ref() + .map_or(false, |block| block.generics.contains(generic)), + Self::ParentFunctionHasAttribute(attribute) => function + .function + .iter() + .any(|f| f.attributes.contains(attribute)), + Self::ParentFunctionHasDocComment(comment) => function.function.iter().any(|f| { + f.doc_comments + .iter() + .filter(|doc| comment.contains(*doc)) + .count() + > 0 + }), + Self::ParentFunctionHasLifetime(lifetime) => function + .function + .iter() + .any(|f| f.lifetime.contains(lifetime)), + Self::ParentFunctionHasGeneric(generic) => function + .function + .iter() + .any(|f| f.generics.contains(generic)), + Self::ParentFunctionHasReturnType(return_type) => function + .function + .iter() + .any(|f| f.return_type == Some(return_type.to_string())), + Self::ParentFunctionHasParameterType(parameter_type) => function + .function + .iter() + .any(|f| f.arguments.values().any(|x| x == parameter_type)), + Self::ParentFunctionHasParameterName(parameter_name) => function + .function + .iter() + .any(|f| f.arguments.keys().any(|x| x == parameter_name)), + } + } +} + +impl FunctionTrait for RustFunction { + fn get_tops(&self) -> Vec<(String, usize)> { + let mut tops = Vec::new(); + self.block.as_ref().map_or((), |block| { + tops.push((block.top.clone(), block.lines.0)); + }); + for parent in &self.function { + tops.push((parent.top.clone(), parent.lines.0)); + } + tops + } + + fn get_bottoms(&self) -> Vec<(String, usize)> { + let mut bottoms = Vec::new(); + self.block.as_ref().map_or((), |block| { + bottoms.push((block.bottom.clone(), block.lines.1)); + }); + for parent in &self.function { + bottoms.push((parent.bottom.clone(), parent.lines.1)); + } + bottoms + } + + fn get_total_lines(&self) -> (usize, usize) { + self.block.as_ref().map_or_else( + || { + let mut start = self.lines.0; + let mut end = self.lines.1; + for parent in &self.function { + if parent.lines.0 < start { + start = parent.lines.0; + end = parent.lines.1; + } + } + (start, end) + }, + |block| (block.lines.0, block.lines.1), + ) + } + + impl_function_trait!(RustFunction); +} diff --git a/git-function-history-lib/src/lib.rs b/git-function-history-lib/src/lib.rs index b42547ea..1922cf46 100644 --- a/git-function-history-lib/src/lib.rs +++ b/git-function-history-lib/src/lib.rs @@ -1,39 +1,67 @@ #![warn(clippy::pedantic, clippy::nursery, clippy::cargo)] #![deny(clippy::use_self, rust_2018_idioms)] #![allow( - clippy::missing_panics_doc, clippy::must_use_candidate, - clippy::case_sensitive_file_extension_comparisons, - clippy::match_wildcard_for_single_variants, clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cognitive_complexity, clippy::float_cmp, - clippy::similar_names, - clippy::missing_errors_doc, - clippy::return_self_not_must_use + // clippy::similar_names, + clippy::return_self_not_must_use, + clippy::module_name_repetitions, + clippy::multiple_crate_versions, + clippy::too_many_lines )] - +/// code and function related language +pub mod languages; /// Different types that can extracted from the result of `get_function_history`. pub mod types; -use ra_ap_syntax::{ - ast::{self, HasDocComments, HasGenericParams, HasName}, - AstNode, SourceFile, SyntaxKind, -}; +macro_rules! get_item_from { + ($oid:expr, $repo:expr, $typs:ident) => { + git_repository::hash::ObjectId::from($oid) + .attach(&$repo) + .object()? + .$typs()? + }; +} + +macro_rules! get_item_from_oid_option { + ($oid:expr, $repo:expr, $typs:ident) => { + git_repository::hash::ObjectId::from($oid) + .attach(&$repo) + .object() + .ok()? + .$typs() + .ok() + }; +} +#[cfg(feature = "cache")] +use cached::proc_macro::cached; +use chrono::{DateTime, NaiveDateTime, Utc}; +use languages::{rust, LanguageFilter, PythonFile, RubyFile, RustFile}; #[cfg(feature = "parallel")] -use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}; + +use git_repository::{objs, prelude::ObjectIdExt, ObjectId}; +use std::{error::Error, ops::Sub}; -use std::{collections::HashMap, error::Error, process::Command}; -pub use types::{ - Block, BlockType, CommitFunctions, File, Function, FunctionBlock, FunctionHistory, +// #[cfg(feature = "c_lang")] +// use languages::CFile; +#[cfg(feature = "unstable")] +use languages::GoFile; + +pub use { + languages::Language, + types::{Commit, FileType, FunctionHistory}, }; /// Different filetypes that can be used to ease the process of finding functions using `get_function_history`. +/// path separator is `/`. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum FileType { +pub enum FileFilterType { /// When you have a absolute path to a file. Absolute(String), - /// When you have a relative path to a file and or want to find look in all files match a name. + /// When you have a relative path to a file and or want to find look in all files match a name (aka ends_with). Relative(String), /// When you want to filter only files in a specific directory Directory(String), @@ -41,7 +69,6 @@ pub enum FileType { None, } -// TODO: Add support for filtering by generic parameters, lifetimes, and return types. /// This is filter enum is used when you want to lookup a function with the filter of filter a previous lookup. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Filter { @@ -57,18 +84,16 @@ pub enum Filter { FileRelative(String), /// When you want to filter only files in a specific directory Directory(String), - // when you want to filter by function that are in a specific block (impl, trait, extern) - FunctionInBlock(BlockType), - // when you want to filter by function that are in between specific lines + /// when you want to filter by function that are in between specific lines FunctionInLines(usize, usize), - // when you want filter by a function that has a parent function of a specific name - FunctionWithParent(String), /// when you want to filter by a any commit author name that contains a specific string Author(String), /// when you want to filter by a any commit author email that contains a specific string AuthorEmail(String), // when you want to filter by a a commit message that contains a specific string Message(String), + /// when you want to filter by proggramming language filter + PLFilter(LanguageFilter), /// When you want to filter by nothing. None, } @@ -85,7 +110,7 @@ pub enum Filter { ///
/// If its a relative it will look for a that ends with the name of the file. ///
-/// If its none it will look for all files in the repo that end in .rs. +/// If its none it will look for all files in the repo that end in supported files (depends on features) /// Note: using `FilteType::None` will take a long time to run (especially if you no filters). ///
/// It will then go through the file and find all the functions and blocks in the file. @@ -94,504 +119,501 @@ pub enum Filter { ///
/// It will then return a `FunctionHistory` struct with all the commits with files that have functions that match the name. ///
-/// If no histoy is is available it will error out with `no history found`. +/// If no histoy is is available it will error out with `no history found`, and possibly a reason why. /// /// # examples /// /// ``` -/// use git_function_history::{get_function_history, Filter, FileType}; -/// let t = get_function_history("empty_test", FileType::Absolute("src/test_functions.rs".to_string()), Filter::None); +/// use git_function_history::{get_function_history, Filter, FileFilterType, Language}; +/// let t = get_function_history("empty_test", &FileFilterType::Absolute("src/test_functions.rs".to_string()), &Filter::None, &Language::Rust).unwrap(); /// ``` -#[allow(clippy::too_many_lines)] +/// +/// # Errors +/// +/// If no files were found that match the criteria given, this will return an 'Err' +/// Or if it cannot find or read from a git repository +/// // TODO: split this function into smaller functions pub fn get_function_history( name: &str, - file: &FileType, - filter: Filter, + file: &FileFilterType, + filter: &Filter, + langs: &languages::Language, ) -> Result> { // chack if name is empty if name.is_empty() { Err("function name is empty")?; } - // check if git is installed - Command::new("git").arg("--version").output()?; - // get the commit hitory - let mut command = Command::new("git"); - command.arg("log"); - command.arg("--pretty=%H;%aD;%aN;%aE;%s"); - command.arg("--date=rfc2822"); - match filter { - Filter::CommitHash(hash) => { - command.arg(hash); - command.arg("-n"); - command.arg("1"); - } + // if filter is date list all the dates and find the one that is closest to the date set that to closest_date and when using the first filter check if the date of the commit is equal to the closest_date + let repo = git_repository::discover(".")?; + let th_repo = repo.clone().into_sync(); + let mut tips = vec![]; + let head = repo.head_commit()?; + tips.push(head.id); + let commit_iter = repo.rev_walk(tips); + let commits = commit_iter + .all()? + .filter_map(|i| match i { + Ok(i) => get_item_from_oid_option!(i, &repo, try_into_commit), + Err(_) => None, + }) + .collect::>(); + // find the closest date by using get_git_dates_commits_oxide + let closest_date = match filter { Filter::Date(date) => { - command.arg("--since"); - command.arg(date); - command.arg("--max-count=1"); + let date = DateTime::parse_from_rfc2822(date)?.with_timezone(&Utc); + let date_list = get_git_info()?; + date_list + .iter() + .min_by_key(|elem| { + elem.date.sub(date).num_seconds().abs() + // elem.0.signed_duration_since(date) + }) + .map(|elem| elem.hash.clone()) + .unwrap_to_error_sync("no commits found")? } + Filter::Author(_) + | Filter::AuthorEmail(_) + | Filter::Message(_) + | Filter::None + | Filter::CommitHash(_) => String::new(), Filter::DateRange(start, end) => { - command.arg("--since"); - command.arg(start); - command.arg("--until"); - command.arg(end); - } - Filter::Author(author) => { - command.arg("--author"); - command.arg(author); - } - Filter::AuthorEmail(email) => { - command.arg("--author"); - command.arg(email); - } - Filter::Message(message) => { - command.arg("--grep"); - command.arg(message); + // check if start is before end + // vaildate that the dates are valid + let start = DateTime::parse_from_rfc2822(start)?.with_timezone(&Utc); + let end = DateTime::parse_from_rfc2822(end)?.with_timezone(&Utc); + if start > end { + Err("start date is after end date")?; + } + String::new() } - Filter::None => {} - _ => { - Err("filter not supported")?; + _ => Err("invalid filter")?, + }; + match file { + FileFilterType::Absolute(file) | FileFilterType::Relative(file) => { + // vaildate that the file makes sense with language + let is_supported = langs + .get_file_endings() + .iter() + .any(|i| ends_with_cmp_no_case(file, i)); + if !is_supported { + Err(format!("file {file} is not a {} file", langs.get_names()))?; + } } + FileFilterType::Directory(_) | FileFilterType::None => {} } - let output = command.output()?; - if !output.stderr.is_empty() { - return Err(String::from_utf8(output.stderr)?)?; - } - let stdout = String::from_utf8(output.stdout)?; - let commits = stdout - .lines() - .map(|line| { - let mut parts = line.split(';'); - let id = parts - .next() - .unwrap_to_error("no id found in git command output"); - let date = parts - .next() - .unwrap_to_error("date is missing from git command output"); - let author = parts - .next() - .unwrap_to_error("author is missing from git command output"); - let email = parts - .next() - .unwrap_to_error("email is missing from git command output"); - let message = parts - .next() - .unwrap_to_error("message is missing from git command output"); - Ok((id?, date?, author?, email?, message?)) + let commits = commits + .iter() + .filter_map(|i| { + let tree = i.tree().ok()?.id; + let time = i.time().ok()?; + let time = DateTime::::from_utc( + NaiveDateTime::from_timestamp_opt(time.seconds_since_unix_epoch.into(), 0)?, + Utc, + ); + let authorinfo = i.author().ok()?; + let author = authorinfo.name.to_string(); + let email = authorinfo.email.to_string(); + let messages = i.message().ok()?; + let mut message = messages.title.to_string(); + if let Some(i) = messages.body { + message.push_str(i.to_string().as_str()); + } + let commit = i.id().to_hex().to_string(); + let metadata = (message, commit, author, email, time); + Some((tree, metadata)) }) - .collect::, Box>>()?; - - let mut file_history = FunctionHistory::new(String::from(name), Vec::new()); - let err = "no history found".to_string(); - // check if file is a rust file - if let FileType::Absolute(path) | FileType::Relative(path) = &file { - if !path.ends_with(".rs") { - Err("file is not a rust file")?; - } - } - #[cfg(feature = "parallel")] - let t = commits.par_iter(); - #[cfg(not(feature = "parallel"))] - let t = commits.iter(); - file_history.commit_history = t - .filter_map(|commit| match &file { - FileType::Absolute(path) => match find_function_in_commit(commit.0, path, name) { - Ok(contents) => Some(CommitFunctions::new( - commit.0.to_string(), - vec![File::new(path.to_string(), contents)], - commit.1, - commit.2.to_string(), - commit.3.to_string(), - commit.4.to_string(), - )), - Err(_) => None, - }, - - FileType::Relative(_) => { - match find_function_in_commit_with_filetype(commit.0, name, file) { - Ok(contents) => Some(CommitFunctions::new( - commit.0.to_string(), - contents, - commit.1, - commit.2.to_string(), - commit.3.to_string(), - commit.4.to_string(), - )), - Err(_) => None, + .filter(|(_, metadata)| { + match filter { + Filter::CommitHash(hash) => *hash == metadata.1, + Filter::Date(_) => metadata.1 == closest_date, + Filter::DateRange(start, end) => { + // let date = metadata.4.seconds_since_unix_epoch; + let date = metadata.4; + let start = DateTime::parse_from_rfc2822(start) + .map(|i| i.with_timezone(&Utc)) + .expect("failed to parse start date, edge case shouldn't happen please file a bug to https://github.com/mendelsshop/git_function_history/issues"); + let end = DateTime::parse_from_rfc2822(end) + .map(|i| i.with_timezone(&Utc)) + .expect("failed to parse end date, edge case shouldn't happen please file a bug to https://github.com/mendelsshop/git_function_history/issues"); + start <= date && date <= end } + Filter::Author(author) => *author == metadata.2, + Filter::AuthorEmail(email) => *email == metadata.3, + Filter::Message(message) => { + metadata.0.contains(message) + || message.contains(&metadata.0) + || message == &metadata.0 + } + Filter::None => true, + _ => false, } - - FileType::None | FileType::Directory(_) => { - match find_function_in_commit_with_filetype(commit.0, name, file) { - Ok(contents) => Some(CommitFunctions::new( - commit.0.to_string(), - contents, - commit.1, - commit.2.to_string(), - commit.3.to_string(), - commit.4.to_string(), - )), - Err(_) => None, + }) + .collect::>(); + #[cfg(feature = "parallel")] + let commits = commits.into_par_iter(); + #[cfg(not(feature = "parallel"))] + let commits = commits.iter(); + let commits = commits + .filter_map(|i| { + let tree = sender(i.0, &th_repo.to_thread_local(), name, *langs, file); + match tree { + Ok(tree) => { + if tree.is_empty() { + None?; + } + Some( + Commit::new( + &i.1 .1, + tree, + &i.1 .4.to_rfc2822(), + &i.1 .2, + &i.1 .3, + &i.1 .0, + ) + .ok()?, + ) } + Err(_) => None, } }) - .collect(); - if file_history.commit_history.is_empty() { - return Err(err)?; + .collect::>(); + if commits.is_empty() { + Err("no history found")?; } - Ok(file_history) + let fh = FunctionHistory::new(name.to_string(), commits); + Ok(fh) } -/// List all the commits date in the git history (in rfc2822 format). -pub fn get_git_dates() -> Result, Box> { - let output = Command::new("git") - .args(["log", "--pretty=%aD", "--date", "rfc2822"]) - .output()?; - let output = String::from_utf8(output.stdout)?; - let output = output - .split('\n') - .map(std::string::ToString::to_string) - .collect::>(); - Ok(output) +/// used for the `get_function_history` macro internally (you don't have to touch this) +pub struct MacroOpts<'a> { + pub name: &'a str, + pub file: FileFilterType, + pub filter: Filter, + pub language: Language, } -/// List all the commit hashes in the git history. -pub fn get_git_commit_hashes() -> Result, Box> { - let output = Command::new("git").args(["log", "--pretty=%H"]).output()?; - let output = String::from_utf8(output.stdout)?; - let output = output - .split('\n') - .map(std::string::ToString::to_string) - .collect::>(); - Ok(output) +impl Default for MacroOpts<'_> { + fn default() -> Self { + Self { + name: "", + file: FileFilterType::None, + filter: Filter::None, + language: Language::All, + } + } } -fn find_file_in_commit(commit: &str, file_path: &str) -> Result> { - let commit_history = Command::new("git") - .args(format!("show {}:{}", commit, file_path).split(' ')) - .output()?; - if !commit_history.stderr.is_empty() { - Err(String::from_utf8_lossy(&commit_history.stderr))?; - } - Ok(String::from_utf8_lossy(&commit_history.stdout).to_string()) +fn sender( + id: ObjectId, + repo: &git_repository::Repository, + name: &str, + langs: Language, + file: &FileFilterType, +) -> Result, Box> { + let object = repo.find_object(id)?; + let tree = object.try_into_tree()?; + traverse_tree(&tree, repo, name, "", langs, file) } -#[allow(clippy::too_many_lines)] -// TODO: split this function into smaller functions -fn find_function_in_commit( - commit: &str, - file_path: &str, +fn traverse_tree( + tree: &git_repository::Tree<'_>, + repo: &git_repository::Repository, name: &str, -) -> Result, Box> { - let file_contents = find_file_in_commit(commit, file_path)?; - let mut functions = Vec::new(); - get_function_asts(name, &file_contents, &mut functions); - let mut starts = file_contents - .match_indices('\n') - .map(|x| x.0) - .collect::>(); - starts.push(0); - starts.sort_unstable(); - let map = starts - .iter() - .enumerate() - .collect::>(); - let mut hist = Vec::new(); - for f in &functions { - let stuff = get_stuff(f, &file_contents, &map); - let generics = get_genrerics_and_lifetime(f); - let mut parent = f.syntax().parent(); - let mut parent_fn: Vec = Vec::new(); - let mut parent_block = None; - while let Some(p) = parent.into_iter().next() { - if p.kind() == SyntaxKind::SOURCE_FILE { - break; + path: &str, + langs: Language, + filetype: &FileFilterType, +) -> Result, Box> { + let treee_iter = tree.iter(); + let mut files: Vec<(String, String)> = Vec::new(); + let mut ret = Vec::new(); + for i in treee_iter { + let i = i?; + match &i.mode() { + objs::tree::EntryMode::Tree => { + let new = get_item_from!(i.oid(), &repo, try_into_tree); + let path_new = format!("{path}/{}", i.filename()); + ret.extend(traverse_tree(&new, repo, name, &path_new, langs, filetype)?); } - ast::Fn::cast(p.clone()).map_or_else( - || { - if let Some(block) = ast::Impl::cast(p.clone()) { - let attr = get_doc_comments_and_attrs(&block); - let stuff = get_stuff(&block, &file_contents, &map); - let generics = get_genrerics_and_lifetime(&block); - parent_block = Some(Block { - name: block.self_ty().map(|ty| ty.to_string()), - lifetime: generics.1, - generics: generics.0, - top: stuff.1 .0, - bottom: stuff.1 .1, - block_type: BlockType::Impl, - lines: (stuff.0 .0, stuff.0 .1), - attributes: attr.1, - doc_comments: attr.0, - }); - } else if let Some(block) = ast::Trait::cast(p.clone()) { - let attr = get_doc_comments_and_attrs(&block); - let stuff = get_stuff(&block, &file_contents, &map); - let generics = get_genrerics_and_lifetime(&block); - parent_block = Some(Block { - name: block.name().map(|ty| ty.to_string()), - lifetime: generics.1, - generics: generics.0, - top: stuff.1 .0, - bottom: stuff.1 .1, - block_type: BlockType::Trait, - lines: (stuff.0 .0, stuff.0 .1), - attributes: attr.1, - doc_comments: attr.0, - }); - } else if let Some(block) = ast::ExternBlock::cast(p.clone()) { - let attr = get_doc_comments_and_attrs(&block); - let stuff = get_stuff(&block, &file_contents, &map); - parent_block = Some(Block { - name: block.abi().map(|ty| ty.to_string()), - lifetime: Vec::new(), - generics: Vec::new(), - top: stuff.1 .0, - bottom: stuff.1 .1, - block_type: BlockType::Extern, - lines: (stuff.0 .0, stuff.0 .1), - attributes: attr.1, - doc_comments: attr.0, - }); + objs::tree::EntryMode::Blob => { + let file = format!("{path}/{}", i.filename()); + match &filetype { + FileFilterType::Relative(ref path) => { + if !file.ends_with(path) { + continue; + } } - }, - |function| { - let stuff = get_stuff(&function, &file_contents, &map); - let generics = get_genrerics_and_lifetime(&function); - let attr = get_doc_comments_and_attrs(&function); - parent_fn.push(FunctionBlock { - name: function.name().unwrap().to_string(), - lifetime: generics.1, - generics: generics.0, - top: stuff.1 .0, - bottom: stuff.1 .1, - lines: (stuff.0 .0, stuff.0 .1), - return_type: function.ret_type().map(|ty| ty.to_string()), - arguments: match function.param_list() { - Some(args) => args - .params() - .map(|arg| arg.to_string()) - .collect::>(), - None => Vec::new(), - }, - attributes: attr.1, - doc_comments: attr.0, - }); - }, - ); - parent = p.parent(); + FileFilterType::Absolute(ref path) => { + if &file == path { + continue; + } + } + FileFilterType::Directory(ref path) => { + if !file.contains(path) { + continue; + } + } + FileFilterType::None => match langs { + // #[cfg(feature = "c_lang")] + // Language::C => { + // if ends_with_cmp_no_case(&file, "c") || ends_with_cmp_no_case(&file, "h") { + // files.push(file); + // } + // } + #[cfg(feature = "unstable")] + Language::Go => { + if !ends_with_cmp_no_case(&file, "go") { + continue; + } + } + Language::Python => { + if !ends_with_cmp_no_case(&file, "py") { + continue; + } + } + Language::Rust => { + if !ends_with_cmp_no_case(&file, "rs") { + continue; + } + } + Language::Ruby => { + if !ends_with_cmp_no_case(&file, "rb") { + continue; + } + } + Language::All => { + cfg_if::cfg_if! { + // if #[cfg(feature = "c_lang")] { + // if !(ends_with_cmp_no_case(&file, "c") || ends_with_cmp_no_case(&file, "h") || !ends_with_cmp_no_case(&file, "rs") || ends_with_cmp_no_case(&file, "py") || ends_with_cmp_no_case(&file, "rb")) { + // continue; + // } + // } + // else + if #[cfg(feature = "unstable")] { + if !(ends_with_cmp_no_case(&file, "go") || ends_with_cmp_no_case(&file, "rs") || ends_with_cmp_no_case(&file, "py") || ends_with_cmp_no_case(&file, "rb")){ + continue + } + } + // else if #[cfg(all(feature = "unstable", feature = "c_lang"))] { + // if !(ends_with_cmp_no_case(&file, "go") || ends_with_cmp_no_case(&file, "c") || ends_with_cmp_no_case(&file, "h") || ends_with_cmp_no_case(&file, "rs") || ends_with_cmp_no_case(&file, "py") || ends_with_cmp_no_case(&file, "rb")) { + // continue; + // } + // } + else { + if !(ends_with_cmp_no_case(&file, "rs") || ends_with_cmp_no_case(&file, "py") || ends_with_cmp_no_case(&file, "rb")) { + continue; + } + } + + } + } + }, + } + let obh = repo.find_object(i.oid())?; + let objref = objs::ObjectRef::from_bytes(obh.kind, &obh.data)?; + let blob = objref.into_blob(); + if let Some(blob) = blob { + files.push((file, String::from_utf8_lossy(blob.data).to_string())); + } + } + _ => {} } - let attr = get_doc_comments_and_attrs(f); - let mut start = stuff.0 .0; - let bb = match map[&start] { - 0 => 0, - x => x + 1, - }; - let contents: String = file_contents[bb..f.syntax().text_range().end().into()] - .to_string() - .lines() - .map(|l| { - start += 1; - format!("{}: {}\n", start, l,) - }) - .collect(); - let contents = contents.trim_end().to_string(); - let function = Function { - name: f.name().unwrap().to_string(), - contents, - block: parent_block, - function: parent_fn, - return_type: f.ret_type().map(|ty| ty.to_string()), - arguments: match f.param_list() { - Some(args) => args - .params() - .map(|arg| arg.to_string()) - .collect::>(), - None => Vec::new(), - }, - lifetime: generics.1, - generics: generics.0, - lines: (stuff.0 .0, stuff.0 .1), - attributes: attr.1, - doc_comments: attr.0, - }; - hist.push(function); - } - if hist.is_empty() { - Err("no function found")?; } - Ok(hist) + ret.extend(find_function_in_files_with_commit( + files, + name.to_string(), + langs, + )); + + Ok(ret) } -fn get_function_asts(name: &str, file: &str, functions: &mut Vec) { - let parsed_file = SourceFile::parse(file).tree(); - parsed_file - .syntax() - .descendants() - .filter_map(ast::Fn::cast) - .filter(|function| function.name().unwrap().text() == name) - .for_each(|function| functions.push(function)); +/// macro to get the history of a function +/// wrapper around the `get_function_history` function +/// +/// # examples +/// ```rust +/// use git_function_history::{get_function_history, languages::Language, Filter, FileFilterType}; +/// git_function_history::get_function_history!(name = "main", file = FileFilterType::Relative("src/main.rs".to_string()), filter = Filter::None, language = Language::Rust); +/// ``` +/// +/// everything is optional but the name, and in no particular order +/// +/// ```rust +/// use git_function_history::{get_function_history, FileFilterType}; +/// git_function_history::get_function_history!(name = "main", file = FileFilterType::Relative("src/main.rs".to_string())); +/// ``` +/// +/// ```rust +/// +/// use git_function_history::{get_function_history, Filter, FileFilterType}; +/// git_function_history::get_function_history!(name = "main", filter = Filter::None, file = FileFilterType::Relative("src/main.rs".to_string())); +/// ``` +/// +/// Default values are: +/// +/// - file: `FileFilterType::None` +/// - filter: `Filter::None` +/// - language: `Language::All` +#[macro_export] +macro_rules! get_function_history { + ($($variant:ident = $value:expr),*) => {{ + let mut opts = $crate::MacroOpts::default(); + $( + opts.$variant = $value; + )* + get_function_history( + opts.name, + &opts.file, + &opts.filter, + &opts.language + ) + }}; } -fn get_stuff( - block: &T, - file: &str, - map: &HashMap, -) -> ((usize, usize), (String, String), (usize, usize)) { - let start = block.syntax().text_range().start(); - let end = block.syntax().text_range().end(); - // get the start and end lines - let mut found_start_brace = 0; - let mut end_line = 0; - let mut starts = 0; - let mut start_line = 0; - // TODO: combine these loops - for (i, line) in file.chars().enumerate() { - if line == '\n' { - if usize::from(start) < i { - starts = i; - break; - } - start_line += 1; - } - } - for (i, line) in file.chars().enumerate() { - if line == '\n' { - if usize::from(end) < i { - break; +/// Returns a vec of information such as author, date, email, and message for each commit +/// +/// # Errors +/// wiil return `Err`if it cannot find or read from a git repository + +pub fn get_git_info() -> Result, Box> { + let repo = git_repository::discover(".")?; + let mut tips = vec![]; + let head = repo.head_commit()?; + tips.push(head.id); + let commit_iter = repo.rev_walk(tips); + let commits = commit_iter.all()?.filter_map(|i| match i { + Ok(i) => get_item_from_oid_option!(i, &repo, try_into_commit).map(|i| { + let Ok(author) = i.author() else { return None }; + let Ok(message) = i.message() else { return None }; + let mut msg = message.title.to_string(); + if let Some(msg_body) = message.body { + msg.push_str(&msg_body.to_string()); } - end_line += 1; - } - if line == '{' && found_start_brace == 0 && usize::from(start) < i { - found_start_brace = i; - } - } - if found_start_brace == 0 { - found_start_brace = usize::from(start); - } - let start = map[&start_line]; - let mut start_lines = start_line; - let mut content: String = file[(*start)..=found_start_brace].to_string(); - if &content[..1] == "\n" { - content = content[1..].to_string(); - } - ( - (start_line, end_line), - ( - content - .lines() - .map(|l| { - start_lines += 1; - format!("{}: {}\n", start_lines, l,) - }) - .collect::() - .trim_end() - .to_string(), - format!( - "\n{}: {}", - end_line, - file.lines() - .nth(if end_line == file.lines().count() - 1 { - end_line - } else { - end_line - 1 - }) - .unwrap_or("") - ), - ), - (starts, end_line), - ) -} -fn get_genrerics_and_lifetime(block: &T) -> (Vec, Vec) { - match block.generic_param_list() { - None => (vec![], vec![]), - Some(gt) => ( - gt.generic_params() - .map(|gt| gt.to_string()) - .collect::>(), - gt.lifetime_params() - .map(|lt| lt.to_string()) - .collect::>(), - ), - } + Some(CommitInfo { + date: match i.time().map(|x| { + Some(DateTime::::from_utc( + NaiveDateTime::from_timestamp_opt(x.seconds_since_unix_epoch.into(), 0)?, + Utc, + )) + }) { + Ok(Some(i)) => i, + _ => return None, + }, + hash: i.id.to_string(), + author_email: author.email.to_string(), + author: author.name.to_string(), + message: msg, + }) + }), + Err(_) => None, + }); + let commits = commits.flatten(); + Ok(commits.collect()) } -fn get_doc_comments_and_attrs(block: &T) -> (Vec, Vec) { - ( - block - .doc_comments() - .map(|c| c.to_string()) - .collect::>(), - block - .attrs() - .map(|c| c.to_string()) - .collect::>(), - ) +pub struct CommitInfo { + pub date: DateTime, + pub hash: String, + pub message: String, + pub author: String, + pub author_email: String, } -fn find_function_in_commit_with_filetype( - commit: &str, +fn find_function_in_file_with_commit( + file_path: &str, + fc: &str, name: &str, - filetype: &FileType, -) -> Result, Box> { - // get a list of all the files in the repository - let mut files = Vec::new(); - let command = Command::new("git") - .args(["ls-tree", "-r", "--name-only", "--full-tree", commit]) - .output()?; - if !command.stderr.is_empty() { - Err(String::from_utf8_lossy(&command.stderr))?; - } - let file_list = String::from_utf8_lossy(&command.stdout).to_string(); - for file in file_list.split('\n') { - match filetype { - FileType::Relative(ref path) => { - if file.ends_with(path) { - files.push(file); - } + langs: Language, +) -> Result> { + let file = match langs { + Language::Rust => { + let functions = rust::find_function_in_file(fc, name)?; + FileType::Rust(RustFile::new(file_path.to_string(), functions)) + } + // #[cfg(feature = "c_lang")] + // Language::C => { + // let functions = languages::c::find_function_in_file(fc, name)?; + // FileType::C(CFile::new(file_path.to_string(), functions)) + // } + #[cfg(feature = "unstable")] + Language::Go => { + let functions = languages::go::find_function_in_file(fc, name)?; + FileType::Go(GoFile::new(file_path.to_string(), functions)) + } + Language::Python => { + let functions = languages::python::find_function_in_file(fc, name)?; + FileType::Python(PythonFile::new(file_path.to_string(), functions)) + } + Language::Ruby => { + let functions = languages::ruby::find_function_in_file(fc, name)?; + FileType::Ruby(RubyFile::new(file_path.to_string(), functions)) + } + Language::All => match file_path.split('.').last() { + Some("rs") => { + let functions = rust::find_function_in_file(fc, name)?; + FileType::Rust(RustFile::new(file_path.to_string(), functions)) } - FileType::Directory(ref path) => { - if path.contains(path) { - files.push(file); - } + // #[cfg(feature = "c_lang")] + // Some("c" | "h") => { + // let functions = languages::c::find_function_in_file(fc, name)?; + // FileType::C(CFile::new(file_path.to_string(), functions)) + // } + Some("py" | "pyw") => { + let functions = languages::python::find_function_in_file(fc, name)?; + FileType::Python(PythonFile::new(file_path.to_string(), functions)) } - FileType::None => { - if file.ends_with(".rs") { - files.push(file); - } + #[cfg(feature = "unstable")] + Some("go") => { + let functions = languages::go::find_function_in_file(fc, name)?; + FileType::Go(GoFile::new(file_path.to_string(), functions)) } - _ => {} - } - } - let err = "no function found".to_string(); - #[cfg(feature = "parellel")] + Some("rb") => { + let functions = languages::ruby::find_function_in_file(fc, name)?; + FileType::Ruby(RubyFile::new(file_path.to_string(), functions)) + } + _ => Err("unknown file type")?, + }, + }; + Ok(file) +} + +#[cfg_attr(feature = "cache", cached)] +// function that takes a vec of files paths and there contents and a function name and uses find_function_in_file_with_commit to find the function in each file and returns a vec of the functions +fn find_function_in_files_with_commit( + files: Vec<(String, String)>, + name: String, + langs: Language, +) -> Vec { + #[cfg(feature = "parallel")] let t = files.par_iter(); - #[cfg(not(feature = "parellel"))] + #[cfg(not(feature = "parallel"))] let t = files.iter(); - let returns: Vec = t - .filter_map(|file| match find_function_in_commit(commit, file, name) { - Ok(functions) => Some(File::new((*file).to_string(), functions)), - Err(_) => None, - }) - .collect(); - if returns.is_empty() { - Err(err)?; - } - Ok(returns) + t.filter_map(|(file_path, fc)| { + find_function_in_file_with_commit(file_path, fc, &name, langs).ok() + }) + .collect() } -trait UwrapToError { - fn unwrap_to_error(self, message: &str) -> Result>; +fn ends_with_cmp_no_case(filename: &str, file_ext: &str) -> bool { + let filename = std::path::Path::new(filename); + filename + .extension() + .map_or(false, |ext| ext.eq_ignore_ascii_case(file_ext)) } -impl UwrapToError for Option { - fn unwrap_to_error(self, message: &str) -> Result> { - match self { - Some(val) => Ok(val), - None => Err(message.to_string().into()), - } +trait UnwrapToError { + fn unwrap_to_error_sync(self, message: &str) -> Result>; + fn unwrap_to_error(self, message: &str) -> Result>; +} + +impl UnwrapToError for Option { + fn unwrap_to_error_sync(self, message: &str) -> Result> { + self.map_or_else(|| Err(message)?, |val| Ok(val)) + } + fn unwrap_to_error(self, message: &str) -> Result> { + self.map_or_else(|| Err(message)?, |val| Ok(val)) } } @@ -599,22 +621,28 @@ impl UwrapToError for Option { mod tests { use chrono::Utc; + use crate::languages::{ + rust::{BlockType, RustFilter}, + FileTrait, + }; + use super::*; #[test] fn found_function() { let now = Utc::now(); let output = get_function_history( "empty_test", - &FileType::Relative("src/test_functions.rs".to_string()), - Filter::None, + &FileFilterType::Relative("src/test_functions.rs".to_string()), + &Filter::None, + &languages::Language::Rust, ); let after = Utc::now() - now; println!("time taken: {}", after.num_seconds()); match &output { Ok(functions) => { - println!("{}", functions); + println!("{functions}"); } - Err(e) => println!("{}", e), + Err(e) => println!("{e}"), } assert!(output.is_ok()); } @@ -622,8 +650,9 @@ mod tests { fn git_installed() { let output = get_function_history( "empty_test", - &FileType::Absolute("src/test_functions.rs".to_string()), - Filter::None, + &FileFilterType::Absolute("src/test_functions.rs".to_string()), + &Filter::None, + &languages::Language::Rust, ); // assert that err is "not git is not installed" if output.is_err() { @@ -635,12 +664,13 @@ mod tests { fn not_found() { let output = get_function_history( "Not_a_function", - &FileType::Absolute("src/test_functions.rs".to_string()), - Filter::None, + &FileFilterType::None, + &Filter::None, + &languages::Language::Rust, ); match &output { - Ok(output) => println!("{}", output), - Err(error) => println!("{}", error), + Ok(output) => println!("{output}"), + Err(error) => println!("{error}"), } assert!(output.is_err()); } @@ -649,46 +679,239 @@ mod tests { fn not_rust_file() { let output = get_function_history( "empty_test", - &FileType::Absolute("src/test_functions.txt".to_string()), - Filter::None, + &FileFilterType::Absolute("src/test_functions.txt".to_string()), + &Filter::None, + &languages::Language::Rust, ); assert!(output.is_err()); - assert_eq!(output.unwrap_err().to_string(), "file is not a rust file"); + println!("{}", output.as_ref().unwrap_err()); + assert!(output + .unwrap_err() + .to_string() + .contains("is not a rust file")); } #[test] - fn test_date() { + fn test_date_range() { let now = Utc::now(); let output = get_function_history( "empty_test", - &FileType::None, - Filter::DateRange( - "17 Aug 2022 11:27:23 -0400".to_owned(), - "19 Aug 2022 23:45:52 +0000".to_owned(), + &FileFilterType::None, + &Filter::DateRange( + "27 Sep 2022 11:27:23 -0400".to_owned(), + "04 Oct 2022 23:45:52 +0000".to_owned(), ), + &languages::Language::Rust, + ); + let after = Utc::now() - now; + println!("time taken: {}", after.num_seconds()); + match &output { + Ok(functions) => { + println!("{functions}"); + } + Err(e) => println!("-{e}-"), + } + assert!(output.is_ok()); + } + + #[test] + fn test_date() { + let now = Utc::now(); + let output = get_function_history( + "empty_test", + &FileFilterType::None, + &Filter::Date("27 Sep 2022 00:27:23 -0400".to_owned()), + &languages::Language::Rust, + ); + let after = Utc::now() - now; + println!("time taken: {}", after.num_seconds()); + match &output { + Ok(functions) => { + println!("{functions}"); + } + Err(e) => println!("-{e}-"), + } + assert!(output.is_ok()); + } + + #[test] + fn expensive_test() { + let now = Utc::now(); + let output = get_function_history( + "empty_test", + &FileFilterType::None, + &Filter::None, + &languages::Language::All, + ); + let after = Utc::now() - now; + println!("time taken: {}", after.num_seconds()); + match &output { + Ok(functions) => { + println!("{functions}"); + functions + .get_commit() + .unwrap() + .files + .iter() + .for_each(|file| { + println!("file: {}", file.get_file_name()); + println!("{file}"); + }); + } + Err(e) => println!("{e}"), + } + assert!(output.is_ok()); + } + + #[test] + fn python_whole() { + let now = Utc::now(); + let output = get_function_history( + "empty_test", + &FileFilterType::Relative("src/test_functions.py".to_string()), + &Filter::None, + &languages::Language::Python, ); let after = Utc::now() - now; println!("time taken: {}", after.num_seconds()); match &output { Ok(functions) => { - println!("{}", functions); + println!("{functions}"); } - Err(e) => println!("-{}-", e), + Err(e) => println!("{e}"), } assert!(output.is_ok()); + let output = output.unwrap(); + let commit = output.get_commit().unwrap(); + let file = commit.get_file().unwrap(); + let _functions = file.get_functions(); } #[test] - fn expensive_tes() { + fn ruby_whole() { let now = Utc::now(); - let output = get_function_history("empty_test", &FileType::None, Filter::None); + let output = get_function_history( + "empty_test", + &FileFilterType::Relative("src/test_functions.rb".to_string()), + &Filter::None, + &languages::Language::Ruby, + ); let after = Utc::now() - now; println!("time taken: {}", after.num_seconds()); match &output { Ok(functions) => { - println!("{}", functions); + println!("{functions}"); } - Err(e) => println!("{}", e), + Err(e) => println!("{e}"), + } + assert!(output.is_ok()); + let output = output.unwrap(); + let commit = output.get_commit().unwrap(); + let file = commit.get_file().unwrap(); + let _functions = file.get_functions(); + } + + // #[test] + // #[cfg(feature = "c_lang")] + // fn c_lang() { + // let now = Utc::now(); + // let output = get_function_history( + // "empty_test", + // &FileFilterType::Relative("src/test_functionsc".to_string()), + // &Filter::DateRange( + // "03 Oct 2022 11:27:23 -0400".to_owned(), + // "05 Oct 2022 23:45:52 +0000".to_owned(), + // ), + // &languages::Language::C, + // ); + // let after = Utc::now() - now; + // println!("time taken: {}", after.num_seconds()); + // match &output { + // Ok(functions) => println!("{}", functions), + // Err(e) => println!("{}", e), + // } + // assert!(output.is_ok()); + // } + #[test] + #[cfg(feature = "unstable")] + fn go_whole() { + let now = Utc::now(); + let output = get_function_history( + "empty_test", + &FileFilterType::Relative("src/test_functions.go".to_string()), + &Filter::None, + &languages::Language::Go, + ); + let after = Utc::now() - now; + println!("time taken: {}", after.num_seconds()); + match &output { + Ok(functions) => println!("{functions}"), + Err(e) => println!("{e}"), } assert!(output.is_ok()); } + + #[test] + fn filter_by_param_rust() { + // search for rust functions + let mut now = Utc::now(); + let output = get_function_history!(name = "empty_test", language = Language::Rust); + let mut after = Utc::now() - now; + println!("time taken to search: {}", after.num_seconds()); + let output = match output { + Ok(result) => result, + Err(e) => panic!("{}", e), + }; + now = Utc::now(); + let new_output = output.filter_by(&Filter::PLFilter(LanguageFilter::Rust( + rust::RustFilter::HasParameterType(String::from("String")), + ))); + after = Utc::now() - now; + println!("time taken to filter {}", after.num_seconds()); + match &new_output { + Ok(res) => println!("{res}"), + Err(e) => println!("{e}"), + } + let new_output = output.filter_by(&Filter::PLFilter(LanguageFilter::Rust( + rust::RustFilter::InBlock(BlockType::Extern), + ))); + after = Utc::now() - now; + println!("time taken to filter {}", after.num_seconds()); + match &new_output { + Ok(res) => println!("{res}"), + Err(e) => println!("{e}"), + } + assert!(new_output.is_ok()); + } + + #[test] + fn test_filter_by() { + let repo = + get_function_history!(name = "empty_test").expect("Failed to get function history"); + let f1 = filter_by!( + repo, + RustFilter::InBlock(crate::languages::rust::BlockType::Impl), + Rust + ); + match f1 { + Ok(_) => println!("filter 1 ok"), + Err(e) => println!("error: {e}"), + } + let f2 = filter_by!( + repo, + Filter::CommitHash("c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0".to_string()) + ); + match f2 { + Ok(_) => println!("filter 2 ok"), + Err(e) => println!("error: {e}"), + } + let f3 = filter_by!( + repo, + LanguageFilter::Rust(RustFilter::InBlock(crate::languages::rust::BlockType::Impl)), + 1 + ); + match f3 { + Ok(_) => println!("filter 3 ok"), + Err(e) => println!("error: {e}"), + } + } } diff --git a/git-function-history-lib/src/test_functions.c b/git-function-history-lib/src/test_functions.c new file mode 100644 index 00000000..cb3910cb --- /dev/null +++ b/git-function-history-lib/src/test_functions.c @@ -0,0 +1,26 @@ +#include + +void test_function(void); + +static void test_function2(void) +{ + printf("Hello World!"); + + // printf("Hello World!" ); +} + +int main() +{ + printf("Hello World!"); + test_function(); + test_function2(); + // test_functions(); + // test_functions2(); + return 0; +} + +void test_function(void) +{ + printf("Hello World!"); +} + diff --git a/git-function-history-lib/src/test_functions.go b/git-function-history-lib/src/test_functions.go new file mode 100644 index 00000000..b6966abc --- /dev/null +++ b/git-function-history-lib/src/test_functions.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" +) + +func main() { + empty_test(1, 2, "3") + fmt.Println("Hello World!") +} + +// doc comment + +func empty_test(c, a int, b string) { + fmt.Println("Hello World!") +} diff --git a/git-function-history-lib/src/test_functions.py b/git-function-history-lib/src/test_functions.py new file mode 100644 index 00000000..a6c0999d --- /dev/null +++ b/git-function-history-lib/src/test_functions.py @@ -0,0 +1,46 @@ +from ctypes.wintypes import PINT + +PINT = 1 + +# def empty_test(): +# print("This is an empty test") + +def assert_that(message): + def decorator(func): + def wrapper(*args, **kwargs): + print(message) + return func(*args, **kwargs) + return wrapper + return decorator + +def test_with_assert(y, n,/,c=7, *, a ,aas = ["a", "b"], **krer,): + @assert_that("This is a test with an assert") + + + + def empty_test(t): + return t == n + + + assert True +class Test: + pass +passing_test = test_with_assert +@assert_that("This is a test with an assert") + +def empty_test(n: int) -> list: + """This is an empty test with a docstring""" + pass + +# def empty_test(n: int) -> list: + +class TestClass: + def test_method(self): + pass + + def test_with_assert(n: Test, a =10,* argss, ** args): + @assert_that("This is a test with an assert") + def empty_test(t): + return t == n + assert True + diff --git a/git-function-history-lib/src/test_functions.rb b/git-function-history-lib/src/test_functions.rb new file mode 100644 index 00000000..be3659bc --- /dev/null +++ b/git-function-history-lib/src/test_functions.rb @@ -0,0 +1,33 @@ +def main() + puts "Hello World" +end +class Banana + class Apple + def empty_test(*a, b, c, d: 1, e: 2, **foo) + end + end + def empty_test(a, c: 1) + end +end +def empty_test(a, **nil) +end +class SupderDuper + def boring + end + def empty_test(a, **nil) + end +end +class + Test < SupderDuper + def empty_test(*b,c, a: ,n: 1, d:true, **foo) + # a,a: ,n: 1, b = true, *bar,**foo + # ) + end + + def test() + puts "Hello World" + end +end + +# def () empty_test +# end diff --git a/git-function-history-lib/src/test_functions.rs b/git-function-history-lib/src/test_functions.rs index e76719ff..dc8588d2 100644 --- a/git-function-history-lib/src/test_functions.rs +++ b/git-function-history-lib/src/test_functions.rs @@ -1,5 +1,5 @@ use std::error::Error; - +use std::fmt::Debug; pub fn empty_test() { } @@ -15,7 +15,7 @@ pub struct Test { pub history: Vec, } -implTest { +implTest { /// empty test pub fn empty_test<'a>() { println!("empty test"); @@ -27,6 +27,9 @@ implTest { } pub fn test_2() { + pub fn empty_test() { + println!("empty test"); + } println!("empty test"); // } } @@ -49,11 +52,11 @@ implTest { } } -#[derive(Debug)] + pub trait super_trait { fn super_trait_method(&self); - fn empty_test() -> String where T: super_trait; + fn empty_test() -> String where T: super_trait; } impl <'a, t> super_trait for t { @@ -62,7 +65,7 @@ impl <'a, t> super_trait for t { /* dff gdg */ - fn empty_test() -> String where T: super_trait { + fn empty_test() -> String where T: super_trait + Clone { String::from("fn empty_test() "); fn broken() { r#"#"}"#; @@ -110,21 +113,33 @@ pub fn function_within(t: String) -> Result> { empty_test(t) } -pub struct Test2 -where T: super_trait { +pub struct Test2 +where +A: +super_trait { pub contents: String, - pub history: Vec, + pub history: Vec, } -impl Test2 -where T: super_trait { - pub fn empty_test<'a>() { +impl Test2 +where A: +super_trait + Clone, +A: Debug + Clone + +{ + pub fn empty_test<'a>() where 'a: Debug { println!("empty test"); } } +mod c { + extern "C" { + pub fn empty_test(t: String); + } +} -extern "C" { - pub fn empty_test(t: String); + +fn main() { + println!("Hello, world!"); } \ No newline at end of file diff --git a/git-function-history-lib/src/types.rs b/git-function-history-lib/src/types.rs index cf970b9f..a26b6184 100644 --- a/git-function-history-lib/src/types.rs +++ b/git-function-history-lib/src/types.rs @@ -1,409 +1,129 @@ use chrono::{DateTime, FixedOffset}; #[cfg(feature = "parallel")] use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; -use std::{collections::HashMap, error::Error, fmt}; +use std::{ + collections::HashMap, + error::Error, + fmt::{self, Display, Formatter}, +}; -use crate::Filter; +use crate::{ + languages::{FileTrait, FunctionTrait, PythonFile, RubyFile, RustFile}, + Filter, +}; -/// This holds the information about a single function each commit will have multiple of these. -#[derive(Debug, Clone)] -pub struct Function { - pub(crate) name: String, - /// The actual code of the function - pub(crate) contents: String, - /// is the function in a block ie `impl` `trait` etc - pub(crate) block: Option, - /// optional parent functions - pub(crate) function: Vec, - /// The line number the function starts and ends on - pub(crate) lines: (usize, usize), - /// The lifetime of the function - pub(crate) lifetime: Vec, - /// The generic types of the function - pub(crate) generics: Vec, - /// The arguments of the function - pub(crate) arguments: Vec, - /// The return type of the function - pub(crate) return_type: Option, - /// The functions atrributes - pub(crate) attributes: Vec, - /// the functions doc comments - pub(crate) doc_comments: Vec, -} - -impl Function { - /// This is a formater almost like the fmt you use for println!, but it takes a previous and next function. - /// This is usefull for printing `CommitHistory` or a vector of functions, because if you use plain old fmt, you can get repeated lines impls, and parent function in your output. - pub fn fmt_with_context( - &self, - f: &mut fmt::Formatter<'_>, - previous: Option<&Self>, - next: Option<&Self>, - ) -> fmt::Result { - match &self.block { - None => {} - Some(block) => match previous { - None => write!(f, "{}\n...\n", block.top)?, - Some(previous_function) => match &previous_function.block { - None => write!(f, "{}\n...\n", block.top)?, - Some(previous_block) => { - if previous_block.lines == block.lines { - } else { - write!(f, "{}\n...\n", block.top)?; - } - } - }, - }, - }; - if !self.function.is_empty() { - for i in &self.function { - match previous { - None => write!(f, "{}\n...\n", i.top)?, - Some(previous_function) => { - if previous_function - .function - .iter() - .any(|x| x.lines == i.lines) - { - } else { - write!(f, "{}\n...\n", i.top)?; - } - } - }; - } - } - - write!(f, "{}", self.contents)?; - if !self.function.is_empty() { - for i in &self.function { - match next { - None => write!(f, "\n...{}", i.bottom)?, - Some(next_function) => { - if next_function.function.iter().any(|x| x.lines == i.lines) { - } else { - write!(f, "\n...{}", i.bottom)?; - } - } - }; - } - } - match &self.block { - None => {} - Some(block) => match next { - None => write!(f, "\n...{}", block.bottom)?, - Some(next_function) => match &next_function.block { - None => write!(f, "\n...{}", block.bottom)?, - Some(next_block) => { - if next_block.lines == block.lines { - } else { - write!(f, "\n...{}", block.bottom)?; - } - } - }, - }, - }; - Ok(()) - } +// #[cfg(feature = "c_lang")] +// use crate::languages::CFile; - /// get metadata like line number, number of parent function etc. - pub fn get_metadata(&self) -> HashMap<&str, String> { - let mut map = HashMap::new(); - map.insert("name", self.name.clone()); - map.insert("lines", format!("{:?}", self.lines)); - map.insert("contents", self.contents.clone()); - if let Some(block) = &self.block { - map.insert("block", format!("{}", block.block_type)); - } - map.insert("generics", self.generics.join(",")); - map.insert("arguments", self.arguments.join(",")); - map.insert("lifetime generics", self.lifetime.join(",")); - map.insert("attributes", self.attributes.join(",")); - map.insert("doc comments", self.doc_comments.join(",")); - match &self.return_type { - None => {} - Some(return_type) => { - map.insert("return type", return_type.clone()); - } - }; - map - } - - /// get the parent functions - pub fn get_parent_function(&self) -> Vec { - self.function.clone() - } - - /// get the block of the function - pub fn get_block(&self) -> Option { - self.block.clone() - } -} - -impl fmt::Display for Function { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.block { - None => {} - Some(block) => write!(f, "{}\n...\n", block.top)?, - }; - for i in &self.function { - write!(f, "{}\n...\n", i.top)?; - } - write!(f, "{}", self.contents)?; - for i in &self.function { - write!(f, "\n...\n{}", i.bottom)?; - } - match &self.block { - None => {} - Some(block) => write!(f, "\n...{}", block.bottom)?, - }; - Ok(()) - } -} - -/// This is used for the functions that are being looked up themeselves but store an outer function that may aontains a function that is being looked up. -#[derive(Debug, Clone)] -pub struct FunctionBlock { - /// The name of the function (parent function) - pub(crate) name: String, - /// what the signature of the function is - pub(crate) top: String, - /// what the last line of the function is - pub(crate) bottom: String, - /// The line number the function starts and ends on - pub(crate) lines: (usize, usize), - /// The lifetime of the function - pub(crate) lifetime: Vec, - /// The generic types of the function - pub(crate) generics: Vec, - /// The arguments of the function - pub(crate) arguments: Vec, - /// The return type of the function - pub(crate) return_type: Option, - /// the function atrributes - pub(crate) attributes: Vec, - /// the functions doc comments - pub(crate) doc_comments: Vec, -} - -impl FunctionBlock { - /// get the metadata for this block ie the name of the block, the type of block, the line number the block starts and ends - pub fn get_metadata(&self) -> HashMap { - let mut map = HashMap::new(); - map.insert("name".to_string(), self.name.clone()); - map.insert("lines".to_string(), format!("{:?}", self.lines)); - map.insert("signature".to_string(), self.top.clone()); - map.insert("bottom".to_string(), self.bottom.clone()); - map.insert("generics".to_string(), self.generics.join(",")); - map.insert("arguments".to_string(), self.arguments.join(",")); - map.insert("lifetime generics".to_string(), self.lifetime.join(",")); - map.insert("attributes".to_string(), self.attributes.join(",")); - map.insert("doc comments".to_string(), self.doc_comments.join(",")); - match &self.return_type { - None => {} - Some(return_type) => { - map.insert("return type".to_string(), return_type.clone()); - } - }; - map - } -} +#[cfg(feature = "unstable")] +use crate::languages::GoFile; -/// This holds information about when a function is in an impl/trait/extern block #[derive(Debug, Clone)] -pub struct Block { - /// The name of the block ie for `impl` it would be the type were impling for - pub(crate) name: Option, - /// The signature of the block - pub(crate) top: String, - /// The last line of the block - pub(crate) bottom: String, - /// the type of block ie `impl` `trait` `extern` - pub(crate) block_type: BlockType, - /// The line number the function starts and ends on - pub(crate) lines: (usize, usize), - /// The lifetime of the function - pub(crate) lifetime: Vec, - /// The generic types of the function - pub(crate) generics: Vec, - /// The blocks atrributes - pub(crate) attributes: Vec, - /// the blocks doc comments - pub(crate) doc_comments: Vec, +pub enum FileType { + Rust(RustFile), + Python(PythonFile), + // #[cfg(feature = "c_lang")] + // C(CFile), + #[cfg(feature = "unstable")] + Go(GoFile), + Ruby(RubyFile), } -impl Block { - /// get the metadata for this block ie the name of the block, the type of block, the line number the block starts and ends - pub fn get_metadata(&self) -> HashMap { - let mut map = HashMap::new(); - if let Some(name) = &self.name { - map.insert("name".to_string(), name.to_string()); +impl FileTrait for FileType { + fn get_file_name(&self) -> String { + match self { + Self::Rust(file) => file.get_file_name(), + Self::Python(file) => file.get_file_name(), + // #[cfg(feature = "c_lang")] + // Self::C(file) => file.get_file_name(), + #[cfg(feature = "unstable")] + Self::Go(file) => file.get_file_name(), + Self::Ruby(file) => file.get_file_name(), } - map.insert("block".to_string(), format!("{}", self.block_type)); - map.insert("lines".to_string(), format!("{:?}", self.lines)); - map.insert("signature".to_string(), self.top.clone()); - map.insert("bottom".to_string(), self.bottom.clone()); - map.insert("generics".to_string(), self.generics.join(",")); - map.insert("lifetime generics".to_string(), self.lifetime.join(",")); - map.insert("attributes".to_string(), self.attributes.join(",")); - map.insert("doc comments".to_string(), self.doc_comments.join(",")); - map } -} - -/// This enum is used when filtering commit history only for let say impl and not externs or traits -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum BlockType { - /// This is for `impl` blocks - Impl, - /// This is for `trait` blocks - Extern, - /// This is for `extern` blocks - Trait, - /// This is for code that gets labeled as a block but `get_function_history` can't find a block type - Unknown, -} - -impl BlockType { - /// This is used to get the name of the block type from a string - pub fn from_string(s: &str) -> Self { - match s { - "impl" => Self::Impl, - "extern" => Self::Extern, - "trait" => Self::Trait, - _ => Self::Unknown, + fn get_functions(&self) -> Vec> { + match self { + Self::Rust(file) => file.get_functions(), + Self::Python(file) => file.get_functions(), + // #[cfg(feature = "c_lang")] + // Self::C(file) => file.get_functions(), + #[cfg(feature = "unstable")] + Self::Go(file) => file.get_functions(), + Self::Ruby(file) => file.get_functions(), } } -} -impl fmt::Display for BlockType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn filter_by(&self, filter: &Filter) -> Result> { match self { - Self::Impl => write!(f, "impl"), - Self::Extern => write!(f, "extern"), - Self::Trait => write!(f, "trait"), - Self::Unknown => write!(f, "unknown"), + Self::Rust(file) => { + let filtered = file.filter_by(filter)?; + Ok(Self::Rust(filtered)) + } + Self::Python(file) => { + let filtered = file.filter_by(filter)?; + Ok(Self::Python(filtered)) + } + // #[cfg(feature = "c_lang")] + // Self::C(file) => { + // let filtered = file.filter_by(filter)?; + // Ok(Self::C(filtered)) + // } + #[cfg(feature = "unstable")] + Self::Go(file) => { + let filtered = file.filter_by(filter)?; + Ok(Self::Go(filtered)) + } + Self::Ruby(file) => { + let filtered = file.filter_by(filter)?; + Ok(Self::Ruby(filtered)) + } } } -} -/// This is used to store each individual file in a commit and the associated functions in that file. -#[derive(Debug, Clone)] -pub struct File { - /// The name of the file - pub(crate) name: String, - functions: Vec, - current_pos: usize, -} - -impl File { - /// Create a new file with the given name and functions - pub fn new(name: String, functions: Vec) -> Self { - Self { - name, - functions, - current_pos: 0, + fn get_current(&self) -> Option> { + match self { + Self::Rust(file) => file.get_current(), + Self::Python(file) => file.get_current(), + // #[cfg(feature = "c_lang")] + // Self::C(file) => file.get_current(), + #[cfg(feature = "unstable")] + Self::Go(file) => file.get_current(), + Self::Ruby(file) => file.get_current(), } } - /// returns a new `File` by filtering the current one by the filter specified (does not modify the current one). - /// - /// valid filters are: `Filter::FunctionInBlock`, `Filter::FunctionInLines`, and `Filter::FunctionWithParent`. - pub fn filter_by(&self, filter: &Filter) -> Result> { - let mut vec = Vec::new(); - for function in &self.functions { - match &filter { - Filter::FunctionInBlock(block_type) => { - if let Some(block) = &function.block { - if block.block_type == *block_type { - vec.push(function.clone()); - } - } - } - Filter::FunctionInLines(start, end) => { - if function.lines.0 >= *start && function.lines.1 <= *end { - vec.push(function.clone()); - } - } - Filter::FunctionWithParent(parent) => { - for parents in &function.function { - if parents.name == *parent { - vec.push(function.clone()); - } - } - } - Filter::None => vec.push(function.clone()), - _ => return Err("Filter not available")?, - } - } - if vec.is_empty() { - return Err("No functions found for filter")?; + fn get_language(&self) -> crate::Language { + match self { + Self::Rust(file) => file.get_language(), + Self::Python(file) => file.get_language(), + // #[cfg(feature = "c_lang")] + // Self::C(file) => file.get_language(), + #[cfg(feature = "unstable")] + Self::Go(file) => file.get_language(), + Self::Ruby(file) => file.get_language(), } - Ok(Self { - name: self.name.clone(), - functions: vec, - current_pos: 0, - }) - } - - /// This is used to get the functions in the file - pub const fn get_functions(&self) -> &Vec { - &self.functions - } - - /// This is used to get the functions in the file (mutable) - pub fn get_functions_mut(&mut self) -> &mut Vec { - &mut self.functions - } - - /// This is will get the current function in the file - pub fn get_current_function(&self) -> Option<&Function> { - self.functions.get(self.current_pos) - } - - /// This is will get the current function in the file (mutable) - pub fn get_current_function_mut(&mut self) -> Option<&mut Function> { - self.functions.get_mut(self.current_pos) } } -impl Iterator for File { - type Item = Function; - fn next(&mut self) -> Option { - // get the current function without removing it - let function = self.functions.get(self.current_pos).cloned(); - self.current_pos += 1; - function - } -} - -impl fmt::Display for File { +impl fmt::Display for FileType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (i, function) in self.functions.iter().enumerate() { - write!( - f, - "{}", - match i { - 0 => "", - _ => "\n...\n", - }, - )?; - let previous = match i { - 0 => None, - _ => self.functions.get(i - 1), - }; - let next = self.functions.get(i + 1); - function.fmt_with_context(f, previous, next)?; + match self { + Self::Rust(file) => write!(f, "{file}"), + Self::Python(file) => write!(f, "{file}"), + // #[cfg(feature = "c_lang")] + // Self::C(file) => write!(f, "{}", file), + #[cfg(feature = "unstable")] + Self::Go(file) => write!(f, "{file}"), + Self::Ruby(file) => write!(f, "{file}"), } - Ok(()) } } - /// This holds information like date and commit `commit_hash` and also the list of function found in the commit. #[derive(Debug, Clone)] -pub struct CommitFunctions { +pub struct Commit { commit_hash: String, - files: Vec, - date: DateTime, + pub(crate) files: Vec, + pub(crate) date: DateTime, current_iter_pos: usize, current_pos: usize, author: String, @@ -411,26 +131,30 @@ pub struct CommitFunctions { message: String, } -impl CommitFunctions { - /// Create a new `CommitFunctions` with the given `commit_hash`, functions, and date. +impl Commit { + /// Create a new `Commit` with the given `commit_hash`, functions, and date. + /// + /// # Errors + /// + /// will return `Err` if it cannot parse the date provided. pub fn new( - commit_hash: String, - files: Vec, + commit_hash: &str, + files: Vec, date: &str, - author: String, - email: String, - message: String, - ) -> Self { - Self { - commit_hash, + author: &str, + email: &str, + message: &str, + ) -> Result> { + Ok(Self { + commit_hash: commit_hash.to_string(), files, - date: DateTime::parse_from_rfc2822(date).expect("Failed to parse date"), + date: DateTime::parse_from_rfc2822(date)?, current_pos: 0, current_iter_pos: 0, - author, - email, - message, - } + author: author.to_string(), + email: email.to_string(), + message: message.to_string(), + }) } /// sets the current file to the next file if possible @@ -457,19 +181,19 @@ impl CommitFunctions { map.insert("date".to_string(), self.date.to_rfc2822()); map.insert( "file".to_string(), - self.files[self.current_pos].name.clone(), + self.files.get(self.current_pos).map_or("error occured, could not get filename, no file found\nfile a bug to https://github.com/mendelsshop/git_function_history/issues".to_string(), FileTrait::get_file_name), ); map } /// returns the current file - pub fn get_file(&self) -> &File { - &self.files[self.current_pos] + pub fn get_file(&self) -> Option<&FileType> { + self.files.get(self.current_pos) } /// returns the current file (mutable) - pub fn get_file_mut(&mut self) -> &mut File { - &mut self.files[self.current_pos] + pub fn get_file_mut(&mut self) -> Option<&mut FileType> { + self.files.get_mut(self.current_pos) } /// tells you in which directions you can move through the files in the commit @@ -482,39 +206,57 @@ impl CommitFunctions { } } - /// returns a new `CommitFunctions` by filtering the current one by the filter specified (does not modify the current one). + /// returns a new `Commit` by filtering the current one by the filter specified (does not modify the current one). /// - /// valid filters are: `Filter::FunctionInBlock`, `Filter::FunctionInLines`, `Filter::FunctionWithParent`, and `Filter::FileAbsolute`, `Filter::FileRelative`, and `Filter::Directory`. + /// valid filters are: `Filter::FunctionInLines`, and `Filter::FileAbsolute`, `Filter::FileRelative`, and `Filter::Directory`. + /// + /// # Errors + /// + /// Will result in an `Err` if a non-valid filter is give, or if no results are found for the given filter pub fn filter_by(&self, filter: &Filter) -> Result> { - let mut vec = Vec::new(); - for f in &self.files { - match filter { + match filter { + Filter::FileAbsolute(_) + | Filter::FileRelative(_) + | Filter::Directory(_) + | Filter::FunctionInLines(..) + | Filter::PLFilter(_) => {} + Filter::None => { + return Ok(self.clone()); + } + _ => Err("Invalid filter")?, + } + #[cfg(feature = "parallel")] + let t = self.files.iter(); + #[cfg(not(feature = "parallel"))] + let t = self.files.iter(); + let vec: Vec<_> = t + .filter_map(|f| match filter { Filter::FileAbsolute(file) => { - if f.name == *file { - vec.push(f.clone()); + if f.get_file_name() == *file { + Some(f.clone()) + } else { + None } } Filter::FileRelative(file) => { - if f.name.ends_with(file) { - vec.push(f.clone()); + if f.get_file_name().ends_with(file) { + Some(f.clone()) + } else { + None } } Filter::Directory(dir) => { - if f.name.contains(dir) { - vec.push(f.clone()); - } - } - Filter::FunctionInLines(..) - | Filter::FunctionWithParent(_) - | Filter::FunctionInBlock(_) => { - if f.filter_by(filter).is_ok() { - vec.push(f.clone()); + if f.get_file_name().contains(dir) { + Some(f.clone()) + } else { + None } } - Filter::None => vec.push(f.clone()), - _ => Err("Invalid filter")?, - } - } + Filter::FunctionInLines(..) | Filter::PLFilter(_) => f.filter_by(filter).ok(), + _ => None, + }) + .collect(); + if vec.is_empty() { return Err("No files found for filter")?; } @@ -531,8 +273,8 @@ impl CommitFunctions { } } -impl Iterator for CommitFunctions { - type Item = File; +impl Iterator for Commit { + type Item = FileType; fn next(&mut self) -> Option { // get the current function without removing it let function = self.files.get(self.current_iter_pos).cloned(); @@ -541,9 +283,16 @@ impl Iterator for CommitFunctions { } } -impl fmt::Display for CommitFunctions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.files[self.current_pos])?; +impl Display for Commit { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!( + f, + "{}", + match self.files.get(self.current_pos) { + Some(file) => file, + None => return Err(fmt::Error), + } + )?; Ok(()) } } @@ -552,14 +301,15 @@ impl fmt::Display for CommitFunctions { #[derive(Debug, Clone)] pub struct FunctionHistory { pub(crate) name: String, - pub(crate) commit_history: Vec, + pub(crate) commit_history: Vec, + current_iter_pos: usize, current_pos: usize, } impl FunctionHistory { // creates a new `FunctionHistory` from a list of commits - pub fn new(name: String, commit_history: Vec) -> Self { + pub fn new(name: String, commit_history: Vec) -> Self { Self { name, commit_history, @@ -576,49 +326,61 @@ impl FunctionHistory { } /// this will move to the next commit if possible - pub fn move_forward(&mut self) { + pub fn move_forward(&mut self) -> Option<()> { if self.current_pos >= self.commit_history.len() - 1 { - return; + return None; } self.current_pos += 1; - self.commit_history[self.current_pos].current_iter_pos = 0; - self.commit_history[self.current_pos].current_pos = 0; + self.commit_history + .get_mut(self.current_pos)? + .current_iter_pos = 0; + self.commit_history.get_mut(self.current_pos)?.current_pos = 0; + Some(()) } /// this will move to the previous commit if possible - pub fn move_back(&mut self) { + pub fn move_back(&mut self) -> Option<()> { if self.current_pos == 0 { - return; + return None; } self.current_pos -= 1; - self.commit_history[self.current_pos].current_iter_pos = 0; - self.commit_history[self.current_pos].current_pos = 0; + self.commit_history + .get_mut(self.current_pos)? + .current_iter_pos = 0; + self.commit_history.get_mut(self.current_pos)?.current_pos = 0; + Some(()) } /// this will move to the next file in the current commit if possible pub fn move_forward_file(&mut self) { - self.commit_history[self.current_pos].move_forward(); + self.commit_history + .get_mut(self.current_pos) + .map(Commit::move_forward); } /// this will move to the previous file in the current commit if possible pub fn move_back_file(&mut self) { - self.commit_history[self.current_pos].move_back(); + self.commit_history + .get_mut(self.current_pos) + .map(Commit::move_back); } /// this returns some metadata about the current commit /// including the `commit hash`, `date`, and `file` pub fn get_metadata(&self) -> HashMap { - self.commit_history[self.current_pos].get_metadata() + self.commit_history + .get(self.current_pos) + .map_or_else(HashMap::new, Commit::get_metadata) } /// returns a mutable reference to the current commit - pub fn get_mut_commit(&mut self) -> &mut CommitFunctions { - &mut self.commit_history[self.current_pos] + pub fn get_mut_commit(&mut self) -> Option<&mut Commit> { + self.commit_history.get_mut(self.current_pos) } /// returns a reference to the current commit - pub fn get_commit(&self) -> &CommitFunctions { - &self.commit_history[self.current_pos] + pub fn get_commit(&self) -> Option<&Commit> { + self.commit_history.get(self.current_pos) } /// returns the directions in which ways you can move through the commit history @@ -633,7 +395,9 @@ impl FunctionHistory { /// tells you in which directions you can move through the files in the current commit pub fn get_commit_move_direction(&self) -> Directions { - self.commit_history[self.current_pos].get_move_direction() + self.commit_history + .get(self.current_pos) + .map_or(Directions::None, Commit::get_move_direction) } /// returns a new `FunctionHistory` by filtering the current one by the filter specified (does not modify the current one). @@ -641,44 +405,75 @@ impl FunctionHistory { /// /// # examples /// ```rust - /// use git_function_history::{get_function_history, Filter, FileType}; + /// use git_function_history::{get_function_history, Filter, FileFilterType, Language}; /// - /// let history = get_function_history("new", FileType::None, Filter::None).unwrap(); + /// let history = get_function_history("new", &FileFilterType::None, &Filter::None, &Language::Rust).unwrap(); /// - /// history.filter_by(Filter::Directory("app".to_string())).unwrap(); + /// history.filter_by(&Filter::Directory("app".to_string())).unwrap(); /// ``` + /// + /// # Errors + /// + /// returns `Err` if no files or commits are match the filter specified pub fn filter_by(&self, filter: &Filter) -> Result> { #[cfg(feature = "parallel")] let t = self.commit_history.par_iter(); #[cfg(not(feature = "parallel"))] let t = self.commit_history.iter(); - let vec: Vec = t - .filter(|f| match filter { + let vec: Vec = t + .filter_map(|f| match filter { Filter::FunctionInLines(..) - | Filter::FunctionWithParent(_) - | Filter::FunctionInBlock(_) | Filter::Directory(_) | Filter::FileAbsolute(_) - | Filter::FileRelative(_) => f.filter_by(filter).is_ok(), - Filter::CommitHash(commit_hash) => &f.commit_hash == commit_hash, - Filter::Date(date) => &f.date.to_rfc2822() == date, + | Filter::PLFilter(_) + | Filter::FileRelative(_) => f.filter_by(filter).ok(), + Filter::CommitHash(commit_hash) => { + if &f.commit_hash == commit_hash { + Some(f.clone()) + } else { + None + } + } + + Filter::Date(date) => { + if &f.date.to_rfc2822() == date { + Some(f.clone()) + } else { + None + } + } Filter::DateRange(start, end) => { - let start = match DateTime::parse_from_rfc2822(start) { - Ok(date) => date, - Err(_) => return false, - }; - let end = match DateTime::parse_from_rfc2822(end) { - Ok(date) => date, - Err(_) => return false, - }; - f.date >= start || f.date <= end + let Ok(start) = DateTime::parse_from_rfc2822(start) else { return None }; + let Ok(end) = DateTime::parse_from_rfc2822(end) else { return None }; + if f.date >= start || f.date <= end { + Some(f.clone()) + } else { + None + } } - Filter::Author(author) => &f.author == author, - Filter::AuthorEmail(email) => &f.email == email, - Filter::Message(message) => f.message.contains(message), - Filter::None => true, + Filter::Author(author) => { + if &f.author == author { + Some(f.clone()) + } else { + None + } + } + Filter::AuthorEmail(email) => { + if &f.email == email { + Some(f.clone()) + } else { + None + } + } + Filter::Message(message) => { + if f.message.contains(message) { + Some(f.clone()) + } else { + None + } + } + Filter::None => None, }) - .cloned() .collect(); if vec.is_empty() { @@ -693,15 +488,50 @@ impl FunctionHistory { } } -impl fmt::Display for FunctionHistory { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "{}", self.commit_history[self.current_pos])?; +/// Macro to filter a the whole git history, a singe commit, or a file. +/// +/// All variants take the thing to be filtered as the first argument. +/// +/// If you just want to pass in a filter of type `Filter` pass in as the second argument the filter. +/// +/// if you just want to pass in a `LanguageFilter` pass in as the second argument the filter and the final argument literal such as 5 or 'a' or "a". +/// This is just to differentiate between the first two variants of the macro. +/// +/// Finally, if you just want to pass in a specific `LanguageFilter` like `RustFilter` pass in as the second argument the filter +/// and the 3rd argument should the variant of `LanguageFilter` such as `Rust` +#[macro_export] +macro_rules! filter_by { + // option 1: takes a filter + ($self:expr, $filter:expr) => { + $self.filter_by(&$filter) + }; + // option 2: takes a PLFilter variant + ($self:expr, $pl_filter:expr, $cfg:literal) => { + $self.filter_by(&Filter::PLFilter($pl_filter)) + }; + // option 3: takes a language specific filter ie RustFilter and a language ie Rust + ($self:expr, $lang_filter:expr, $language:ident) => {{ + use $crate::languages::LanguageFilter; + $self.filter_by(&Filter::PLFilter(LanguageFilter::$language($lang_filter))) + }}; +} + +impl Display for FunctionHistory { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + writeln!( + f, + "{}", + self.commit_history.get(self.current_pos).map_or( + "could not retrieve commit please file a bug".to_string(), + ToString::to_string + ) + )?; Ok(()) } } impl Iterator for FunctionHistory { - type Item = CommitFunctions; + type Item = Commit; fn next(&mut self) -> Option { self.commit_history .get(self.current_iter_pos) @@ -726,15 +556,12 @@ pub enum Directions { Both, } -trait ErrorToOption { - fn to_option(self) -> Option; +trait ErrorToOption { + fn to_option(self) -> Option; } -impl ErrorToOption for Result> { - fn to_option(self) -> Option { - match self { - Ok(t) => Some(t), - Err(_) => None, - } +impl ErrorToOption for Result> { + fn to_option(self) -> Option { + self.map_or(None, |t| Some(t)) } } diff --git a/resources/msrv.svg b/resources/msrv.svg new file mode 100644 index 00000000..01d90797 --- /dev/null +++ b/resources/msrv.svg @@ -0,0 +1 @@ +minimum rust version: 1.65.0minimum rust version1.65.0 \ No newline at end of file