Skip to content

Commit cfda74d

Browse files
committed
feat: add author filter option for commit display
1 parent 714357e commit cfda74d

4 files changed

Lines changed: 93 additions & 55 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ gitlogue --commit abc123 --loop
9292
# Loop through a commit range
9393
gitlogue --commit HEAD~10..HEAD --loop
9494

95+
# Filter commits by author or email (case-insensitive partial match)
96+
gitlogue --author "john"
97+
9598
# Use a different theme
9699
gitlogue --theme dracula
97100

@@ -111,7 +114,7 @@ gitlogue theme list
111114
gitlogue theme set dracula
112115

113116
# Combine options
114-
gitlogue --commit HEAD~5 --theme nord --speed 15 --ignore "*.ipynb"
117+
gitlogue --commit HEAD~5 --author "john" --theme nord --speed 15 --ignore "*.ipynb"
115118
```
116119

117120
## Configuration

docs/usage.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,26 @@ When using commit ranges:
7575
- Merge commits are automatically excluded
7676
- Use `--loop` to replay the range continuously
7777

78+
### `--author <PATTERN>` / `-a <PATTERN>`
79+
80+
Filter commits by author name or email address. The filter performs a case-insensitive partial match against both the author's name and email.
81+
82+
```bash
83+
# Filter by name
84+
gitlogue --author "John"
85+
gitlogue -a "jane"
86+
87+
# Filter by email
88+
gitlogue --author "@example.com"
89+
gitlogue -a "john.doe@"
90+
91+
# Loop your own commits
92+
gitlogue --author "alice" --loop
93+
```
94+
95+
This is useful for:
96+
- Replaying only your own commits
97+
7898
### `--theme <NAME>`
7999

80100
Select a theme for the UI.

src/git.rs

Lines changed: 55 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,25 @@ pub fn should_exclude_file(path: &str) -> bool {
120120
false
121121
}
122122

123+
// Check if a commit matches the author filter pattern (case-insensitive partial match)
124+
fn matches_author(commit: &Git2Commit, pattern: &str) -> bool {
125+
let author = commit.author();
126+
let name = author.name().unwrap_or("");
127+
let email = author.email().unwrap_or("");
128+
let pattern_lower = pattern.to_lowercase();
129+
130+
name.to_lowercase().contains(&pattern_lower)
131+
|| email.to_lowercase().contains(&pattern_lower)
132+
}
133+
123134
pub struct GitRepository {
124135
repo: Repository,
125136
commit_cache: RefCell<Option<Vec<Oid>>>,
126137
// Shared index for both cache-based playback (asc/desc) and range playback.
127138
// These modes are mutually exclusive based on CLI arguments.
128139
commit_index: RefCell<usize>,
129140
commit_range: RefCell<Option<Vec<Oid>>>,
141+
author_filter: Option<String>,
130142
}
131143

132144
#[derive(Debug, Clone)]
@@ -252,6 +264,7 @@ impl GitRepository {
252264
commit_cache: RefCell::new(None),
253265
commit_index: RefCell::new(0),
254266
commit_range: RefCell::new(None),
267+
author_filter: None,
255268
})
256269
}
257270

@@ -267,35 +280,16 @@ impl GitRepository {
267280
}
268281

269282
pub fn random_commit(&self) -> Result<CommitMetadata> {
270-
// Check if cache exists, if not populate it
271-
let mut cache = self.commit_cache.borrow_mut();
272-
if cache.is_none() {
273-
let mut revwalk = self.repo.revwalk()?;
274-
revwalk.push_head()?;
275-
276-
let mut candidates = Vec::new();
277-
for oid in revwalk.filter_map(|oid| oid.ok()) {
278-
if let Ok(commit) = self.repo.find_commit(oid) {
279-
if commit.parent_count() <= 1 {
280-
candidates.push(oid);
281-
}
282-
}
283-
}
284-
285-
if candidates.is_empty() {
286-
anyhow::bail!("No non-merge commits found in repository");
287-
}
288-
289-
*cache = Some(candidates);
290-
}
283+
self.populate_cache()?;
291284

285+
let cache = self.commit_cache.borrow();
292286
let candidates = cache.as_ref().unwrap();
287+
293288
let selected_oid = candidates
294289
.get(rand::rng().random_range(0..candidates.len()))
295290
.context("Failed to select random commit")?;
296291

297292
let commit = self.repo.find_commit(*selected_oid)?;
298-
drop(cache); // Release the borrow before calling extract_metadata_with_changes
299293
Self::extract_metadata_with_changes(&self.repo, &commit)
300294
}
301295

@@ -323,8 +317,6 @@ impl GitRepository {
323317
*index += 1;
324318

325319
let commit = self.repo.find_commit(*selected_oid)?;
326-
drop(index);
327-
drop(cache);
328320
Self::extract_metadata_with_changes(&self.repo, &commit)
329321
}
330322

@@ -349,15 +341,17 @@ impl GitRepository {
349341
*index += 1;
350342

351343
let commit = self.repo.find_commit(*selected_oid)?;
352-
drop(index);
353-
drop(cache);
354344
Self::extract_metadata_with_changes(&self.repo, &commit)
355345
}
356346

357347
pub fn reset_index(&self) {
358348
*self.commit_index.borrow_mut() = 0;
359349
}
360350

351+
pub fn set_author_filter(&mut self, author: Option<String>) {
352+
self.author_filter = author;
353+
}
354+
361355
pub fn set_commit_range(&self, range: &str) -> Result<()> {
362356
let commits = self.parse_commit_range(range)?;
363357
*self.commit_range.borrow_mut() = Some(commits);
@@ -382,8 +376,6 @@ impl GitRepository {
382376
*index += 1;
383377

384378
let commit = self.repo.find_commit(*selected_oid)?;
385-
drop(index);
386-
drop(range);
387379
Self::extract_metadata_with_changes(&self.repo, &commit)
388380
}
389381

@@ -406,8 +398,6 @@ impl GitRepository {
406398
*index += 1;
407399

408400
let commit = self.repo.find_commit(*selected_oid)?;
409-
drop(index);
410-
drop(range);
411401
Self::extract_metadata_with_changes(&self.repo, &commit)
412402
}
413403

@@ -424,10 +414,42 @@ impl GitRepository {
424414
.context("Failed to select random commit")?;
425415

426416
let commit = self.repo.find_commit(*selected_oid)?;
427-
drop(range);
428417
Self::extract_metadata_with_changes(&self.repo, &commit)
429418
}
430419

420+
// Collect non-merge commits from a revwalk, applying author filter if set
421+
fn collect_commits_from_revwalk(
422+
&self,
423+
revwalk: git2::Revwalk,
424+
context: &str,
425+
) -> Result<Vec<Oid>> {
426+
let mut commits = Vec::new();
427+
for oid in revwalk.filter_map(|oid| oid.ok()) {
428+
if let Ok(commit) = self.repo.find_commit(oid) {
429+
if commit.parent_count() <= 1 {
430+
if let Some(ref pattern) = self.author_filter {
431+
if !matches_author(&commit, pattern) {
432+
continue;
433+
}
434+
}
435+
commits.push(oid);
436+
}
437+
}
438+
}
439+
440+
if commits.is_empty() {
441+
if self.author_filter.is_some() {
442+
anyhow::bail!(
443+
"No commits found matching the author filter {}",
444+
context
445+
);
446+
}
447+
anyhow::bail!("No non-merge commits found {}", context);
448+
}
449+
450+
Ok(commits)
451+
}
452+
431453
fn parse_commit_range(&self, range: &str) -> Result<Vec<Oid>> {
432454
// Reject symmetric difference operator (not supported)
433455
if range.contains("...") {
@@ -467,15 +489,7 @@ impl GitRepository {
467489
revwalk.hide(start_oid)?;
468490
}
469491

470-
let mut commits = Vec::new();
471-
for oid in revwalk.filter_map(|oid| oid.ok()) {
472-
if let Ok(commit) = self.repo.find_commit(oid) {
473-
if commit.parent_count() <= 1 {
474-
commits.push(oid);
475-
}
476-
}
477-
}
478-
492+
let mut commits = self.collect_commits_from_revwalk(revwalk, "in range")?;
479493
commits.reverse();
480494
Ok(commits)
481495
}
@@ -486,19 +500,7 @@ impl GitRepository {
486500
let mut revwalk = self.repo.revwalk()?;
487501
revwalk.push_head()?;
488502

489-
let mut candidates = Vec::new();
490-
for oid in revwalk.filter_map(|oid| oid.ok()) {
491-
if let Ok(commit) = self.repo.find_commit(oid) {
492-
if commit.parent_count() <= 1 {
493-
candidates.push(oid);
494-
}
495-
}
496-
}
497-
498-
if candidates.is_empty() {
499-
anyhow::bail!("No non-merge commits found in repository");
500-
}
501-
503+
let candidates = self.collect_commits_from_revwalk(revwalk, "in repository")?;
502504
*cache = Some(candidates);
503505
}
504506
Ok(())

src/main.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ pub struct Args {
9292
#[arg(long, help = "Display third-party license information")]
9393
pub license: bool,
9494

95+
#[arg(
96+
short = 'a',
97+
long,
98+
value_name = "PATTERN",
99+
help = "Filter commits by author name or email (partial match, case-insensitive)"
100+
)]
101+
pub author: Option<String>,
102+
95103
#[arg(
96104
short = 'i',
97105
long = "ignore",
@@ -210,7 +218,12 @@ fn main() -> Result<()> {
210218
}
211219

212220
let repo_path = args.validate()?;
213-
let repo = GitRepository::open(&repo_path)?;
221+
let mut repo = GitRepository::open(&repo_path)?;
222+
223+
// Set author filter if specified
224+
if args.author.is_some() {
225+
repo.set_author_filter(args.author.clone());
226+
}
214227

215228
let is_commit_specified = args.commit.is_some();
216229
let is_range_mode = args

0 commit comments

Comments
 (0)