Skip to content

Commit 3c5131a

Browse files
authored
commit log filtering (#1800)
1 parent b50d44a commit 3c5131a

22 files changed

+1139
-107
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
**search commits**
11+
12+
![commit-search](assets/log-search.gif)
13+
1014
**visualize empty lines in diff better**
1115

1216
![diff-empty-line](assets/diff-empty-line.png)
@@ -20,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2024
* Future additions of colors etc. will not break existing themes anymore
2125

2226
### Added
27+
* search commits by files in diff or commit message ([#1791](https://github.com/extrawurst/gitui/issues/1791))
2328
* support 'n'/'p' key to move to the next/prev hunk in diff component [[@hamflx](https://github.com/hamflx)] ([#1523](https://github.com/extrawurst/gitui/issues/1523))
2429
* simplify theme overrides [[@cruessler](https://github.com/cruessler)] ([#1367](https://github.com/extrawurst/gitui/issues/1367))
2530
* support for sign-off of commits [[@domtac](https://github.com/domtac)]([#1757](https://github.com/extrawurst/gitui/issues/1757))

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ For a [RustBerlin meetup presentation](https://youtu.be/rpilJV-eIVw?t=5334) ([sl
7777

7878
These are the high level goals before calling out `1.0`:
7979

80-
* log search (commit, author, sha) ([#1791](https://github.com/extrawurst/gitui/issues/1791))
8180
* visualize branching structure in log tab ([#81](https://github.com/extrawurst/gitui/issues/81))
8281
* interactive rebase ([#32](https://github.com/extrawurst/gitui/issues/32))
8382

assets/log-search.gif

2.48 MB
Loading

asyncgit/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ categories = ["concurrency", "asynchronous"]
1212
keywords = ["git"]
1313

1414
[dependencies]
15+
bitflags = "1"
1516
crossbeam-channel = "0.5"
1617
easy-cast = "0.5"
18+
fuzzy-matcher = "0.3"
1719
git2 = "0.17"
1820
log = "0.4"
1921
# git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]}

asyncgit/src/revlog.rs

+33-9
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::{
1111
Arc, Mutex,
1212
},
1313
thread,
14-
time::Duration,
14+
time::{Duration, Instant},
1515
};
1616

1717
///
@@ -25,9 +25,16 @@ pub enum FetchStatus {
2525
Started,
2626
}
2727

28+
///
29+
pub struct AsyncLogResult {
30+
///
31+
pub commits: Vec<CommitId>,
32+
///
33+
pub duration: Duration,
34+
}
2835
///
2936
pub struct AsyncLog {
30-
current: Arc<Mutex<Vec<CommitId>>>,
37+
current: Arc<Mutex<AsyncLogResult>>,
3138
current_head: Arc<Mutex<Option<CommitId>>>,
3239
sender: Sender<AsyncGitNotification>,
3340
pending: Arc<AtomicBool>,
@@ -49,7 +56,10 @@ impl AsyncLog {
4956
) -> Self {
5057
Self {
5158
repo,
52-
current: Arc::new(Mutex::new(Vec::new())),
59+
current: Arc::new(Mutex::new(AsyncLogResult {
60+
commits: Vec::new(),
61+
duration: Duration::default(),
62+
})),
5363
current_head: Arc::new(Mutex::new(None)),
5464
sender: sender.clone(),
5565
pending: Arc::new(AtomicBool::new(false)),
@@ -60,7 +70,7 @@ impl AsyncLog {
6070

6171
///
6272
pub fn count(&self) -> Result<usize> {
63-
Ok(self.current.lock()?.len())
73+
Ok(self.current.lock()?.commits.len())
6474
}
6575

6676
///
@@ -69,17 +79,28 @@ impl AsyncLog {
6979
start_index: usize,
7080
amount: usize,
7181
) -> Result<Vec<CommitId>> {
72-
let list = self.current.lock()?;
82+
let list = &self.current.lock()?.commits;
7383
let list_len = list.len();
7484
let min = start_index.min(list_len);
7585
let max = min + amount;
7686
let max = max.min(list_len);
7787
Ok(list[min..max].to_vec())
7888
}
7989

90+
///
91+
pub fn get_items(&self) -> Result<Vec<CommitId>> {
92+
let list = &self.current.lock()?.commits;
93+
Ok(list.clone())
94+
}
95+
96+
///
97+
pub fn get_last_duration(&self) -> Result<Duration> {
98+
Ok(self.current.lock()?.duration)
99+
}
100+
80101
///
81102
pub fn position(&self, id: CommitId) -> Result<Option<usize>> {
82-
let list = self.current.lock()?;
103+
let list = &self.current.lock()?.commits;
83104
let position = list.iter().position(|&x| x == id);
84105

85106
Ok(position)
@@ -160,11 +181,13 @@ impl AsyncLog {
160181

161182
fn fetch_helper(
162183
repo_path: &RepoPath,
163-
arc_current: &Arc<Mutex<Vec<CommitId>>>,
184+
arc_current: &Arc<Mutex<AsyncLogResult>>,
164185
arc_background: &Arc<AtomicBool>,
165186
sender: &Sender<AsyncGitNotification>,
166187
filter: Option<LogWalkerFilter>,
167188
) -> Result<()> {
189+
let start_time = Instant::now();
190+
168191
let mut entries = Vec::with_capacity(LIMIT_COUNT);
169192
let r = repo(repo_path)?;
170193
let mut walker =
@@ -175,7 +198,8 @@ impl AsyncLog {
175198

176199
if !res_is_err {
177200
let mut current = arc_current.lock()?;
178-
current.extend(entries.iter());
201+
current.commits.extend(entries.iter());
202+
current.duration = start_time.elapsed();
179203
}
180204

181205
if res_is_err || entries.len() <= 1 {
@@ -196,7 +220,7 @@ impl AsyncLog {
196220
}
197221

198222
fn clear(&mut self) -> Result<()> {
199-
self.current.lock()?.clear();
223+
self.current.lock()?.commits.clear();
200224
*self.current_head.lock()? = None;
201225
Ok(())
202226
}

asyncgit/src/sync/logwalker.rs

+209-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
#![allow(dead_code)]
12
use super::CommitId;
23
use crate::{error::Result, sync::commit_files::get_commit_diff};
3-
use git2::{Commit, Oid, Repository};
4+
use bitflags::bitflags;
5+
use fuzzy_matcher::FuzzyMatcher;
6+
use git2::{Commit, Diff, Oid, Repository};
47
use std::{
58
cmp::Ordering,
69
collections::{BinaryHeap, HashSet},
@@ -55,6 +58,163 @@ pub fn diff_contains_file(file_path: String) -> LogWalkerFilter {
5558
))
5659
}
5760

61+
bitflags! {
62+
///
63+
pub struct SearchFields: u32 {
64+
///
65+
const MESSAGE = 0b0000_0001;
66+
///
67+
const FILENAMES = 0b0000_0010;
68+
//TODO:
69+
// const COMMIT_HASHES = 0b0000_0100;
70+
// ///
71+
// const DATES = 0b0000_1000;
72+
// ///
73+
// const AUTHORS = 0b0001_0000;
74+
// ///
75+
// const DIFFS = 0b0010_0000;
76+
}
77+
}
78+
79+
impl Default for SearchFields {
80+
fn default() -> Self {
81+
Self::MESSAGE
82+
}
83+
}
84+
85+
bitflags! {
86+
///
87+
pub struct SearchOptions: u32 {
88+
///
89+
const CASE_SENSITIVE = 0b0000_0001;
90+
///
91+
const FUZZY_SEARCH = 0b0000_0010;
92+
}
93+
}
94+
95+
impl Default for SearchOptions {
96+
fn default() -> Self {
97+
Self::empty()
98+
}
99+
}
100+
101+
///
102+
#[derive(Default, Debug, Clone)]
103+
pub struct LogFilterSearchOptions {
104+
///
105+
pub search_pattern: String,
106+
///
107+
pub fields: SearchFields,
108+
///
109+
pub options: SearchOptions,
110+
}
111+
112+
///
113+
#[derive(Default)]
114+
pub struct LogFilterSearch {
115+
///
116+
pub matcher: fuzzy_matcher::skim::SkimMatcherV2,
117+
///
118+
pub options: LogFilterSearchOptions,
119+
}
120+
121+
impl LogFilterSearch {
122+
///
123+
pub fn new(options: LogFilterSearchOptions) -> Self {
124+
let mut options = options;
125+
if !options.options.contains(SearchOptions::CASE_SENSITIVE) {
126+
options.search_pattern =
127+
options.search_pattern.to_lowercase();
128+
}
129+
Self {
130+
matcher: fuzzy_matcher::skim::SkimMatcherV2::default(),
131+
options,
132+
}
133+
}
134+
135+
fn match_diff(&self, diff: &Diff<'_>) -> bool {
136+
diff.deltas().any(|delta| {
137+
if delta
138+
.new_file()
139+
.path()
140+
.and_then(|file| file.as_os_str().to_str())
141+
.map(|file| self.match_text(file))
142+
.unwrap_or_default()
143+
{
144+
return true;
145+
}
146+
147+
delta
148+
.old_file()
149+
.path()
150+
.and_then(|file| file.as_os_str().to_str())
151+
.map(|file| self.match_text(file))
152+
.unwrap_or_default()
153+
})
154+
}
155+
156+
///
157+
pub fn match_text(&self, text: &str) -> bool {
158+
if self.options.options.contains(SearchOptions::FUZZY_SEARCH)
159+
{
160+
self.matcher
161+
.fuzzy_match(
162+
text,
163+
self.options.search_pattern.as_str(),
164+
)
165+
.is_some()
166+
} else if self
167+
.options
168+
.options
169+
.contains(SearchOptions::CASE_SENSITIVE)
170+
{
171+
text.contains(self.options.search_pattern.as_str())
172+
} else {
173+
text.to_lowercase()
174+
.contains(self.options.search_pattern.as_str())
175+
}
176+
}
177+
}
178+
179+
///
180+
pub fn filter_commit_by_search(
181+
filter: LogFilterSearch,
182+
) -> LogWalkerFilter {
183+
Arc::new(Box::new(
184+
move |repo: &Repository,
185+
commit_id: &CommitId|
186+
-> Result<bool> {
187+
let commit = repo.find_commit((*commit_id).into())?;
188+
189+
let msg_match = filter
190+
.options
191+
.fields
192+
.contains(SearchFields::MESSAGE)
193+
.then(|| {
194+
commit.message().map(|msg| filter.match_text(msg))
195+
})
196+
.flatten()
197+
.unwrap_or_default();
198+
199+
let file_match = filter
200+
.options
201+
.fields
202+
.contains(SearchFields::FILENAMES)
203+
.then(|| {
204+
get_commit_diff(
205+
repo, *commit_id, None, None, None,
206+
)
207+
.ok()
208+
})
209+
.flatten()
210+
.map(|diff| filter.match_diff(&diff))
211+
.unwrap_or_default();
212+
213+
Ok(msg_match || file_match)
214+
},
215+
))
216+
}
217+
58218
///
59219
pub struct LogWalker<'a> {
60220
commits: BinaryHeap<TimeOrderedCommit<'a>>,
@@ -130,6 +290,7 @@ impl<'a> LogWalker<'a> {
130290
mod tests {
131291
use super::*;
132292
use crate::error::Result;
293+
use crate::sync::tests::write_commit_file;
133294
use crate::sync::RepoPath;
134295
use crate::sync::{
135296
commit, get_commits_info, stage_add_file,
@@ -246,4 +407,51 @@ mod tests {
246407

247408
Ok(())
248409
}
410+
411+
#[test]
412+
fn test_logwalker_with_filter_search() {
413+
let (_td, repo) = repo_init_empty().unwrap();
414+
415+
write_commit_file(&repo, "foo", "a", "commit1");
416+
let second_commit_id = write_commit_file(
417+
&repo,
418+
"baz",
419+
"a",
420+
"my commit msg (#2)",
421+
);
422+
write_commit_file(&repo, "foo", "b", "commit3");
423+
424+
let log_filter = filter_commit_by_search(
425+
LogFilterSearch::new(LogFilterSearchOptions {
426+
fields: SearchFields::MESSAGE,
427+
options: SearchOptions::FUZZY_SEARCH,
428+
search_pattern: String::from("my msg"),
429+
}),
430+
);
431+
432+
let mut items = Vec::new();
433+
let mut walker = LogWalker::new(&repo, 100)
434+
.unwrap()
435+
.filter(Some(log_filter));
436+
walker.read(&mut items).unwrap();
437+
438+
assert_eq!(items.len(), 1);
439+
assert_eq!(items[0], second_commit_id);
440+
441+
let log_filter = filter_commit_by_search(
442+
LogFilterSearch::new(LogFilterSearchOptions {
443+
fields: SearchFields::FILENAMES,
444+
options: SearchOptions::FUZZY_SEARCH,
445+
search_pattern: String::from("fo"),
446+
}),
447+
);
448+
449+
let mut items = Vec::new();
450+
let mut walker = LogWalker::new(&repo, 100)
451+
.unwrap()
452+
.filter(Some(log_filter));
453+
walker.read(&mut items).unwrap();
454+
455+
assert_eq!(items.len(), 2);
456+
}
249457
}

0 commit comments

Comments
 (0)