From ebec07f245ee8c19794d00af78ed8972caede5c0 Mon Sep 17 00:00:00 2001 From: AjithPanneerselvam Date: Thu, 30 Jan 2025 17:34:03 +0000 Subject: [PATCH] Feature: Introduce oldest() to the revset language to return the oldest commits in a set. --- CHANGELOG.md | 2 ++ docs/revsets.md | 3 ++ lib/src/default_index/revset_engine.rs | 49 +++++++++++++++++++++++++ lib/src/revset.rs | 50 +++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857eeaf711..0af6f6f446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Conditional configuration now supports `--when.commands` to change configuration based on subcommand. +* New `oldest` revset function to get the oldest commit in a set. + ### Fixed bugs * `jj git fetch` with multiple remotes will now fetch from all remotes before diff --git a/docs/revsets.md b/docs/revsets.md index 810fefd5d5..cece701149 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -256,6 +256,9 @@ revsets (expressions) as arguments. * `latest(x[, count])`: Latest `count` commits in `x`, based on committer timestamp. The default `count` is 1. +* `oldest(x[, count])`: Oldest `count` commits in `x`, based on committer + timestamp. The default `count` is 1. + * `fork_point(x)`: The fork point of all commits in `x`. The fork point is the common ancestor(s) of all commits in `x` which do not have any descendants that are also common ancestors of all commits in `x`. It is equivalent to diff --git a/lib/src/default_index/revset_engine.rs b/lib/src/default_index/revset_engine.rs index 80c95ebdee..c812b90ba0 100644 --- a/lib/src/default_index/revset_engine.rs +++ b/lib/src/default_index/revset_engine.rs @@ -974,6 +974,10 @@ impl EvaluationContext<'_> { let candidate_set = self.evaluate(candidates)?; Ok(Box::new(self.take_latest_revset(&*candidate_set, *count)?)) } + ResolvedExpression::Oldest { candidates, count } => { + let candidate_set = self.evaluate(candidates)?; + Ok(Box::new(self.take_oldest_revset(&*candidate_set, *count)?)) + } ResolvedExpression::Coalesce(expression1, expression2) => { let set1 = self.evaluate(expression1)?; if set1.positions().attach(index).next().is_some() { @@ -1057,6 +1061,51 @@ impl EvaluationContext<'_> { Ok(EagerRevset { positions }) } + fn take_oldest_revset( + &self, + candidate_set: &dyn InternalRevset, + count: usize, + ) -> Result { + if count == 0 { + return Ok(EagerRevset::empty()); + } + + #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] + struct Item { + timestamp: MillisSinceEpoch, + pos: IndexPosition, // tie-breaker + } + + let make_rev_item = |pos| -> Result<_, RevsetEvaluationError> { + let entry = self.index.entry_by_pos(pos?); + let commit = self.store.get_commit(&entry.commit_id())?; + Ok(Item { + timestamp: commit.committer().timestamp.timestamp, + pos: entry.position(), + }) + }; + + // Maintain max-heap containing the earliest (smallest) count items. + let mut candidate_iter = candidate_set + .positions() + .attach(self.index) + .map(make_rev_item) + .fuse(); + let mut oldest_items: BinaryHeap<_> = candidate_iter.by_ref().take(count).try_collect()?; + for item in candidate_iter { + let item = item?; + let mut newest = oldest_items.peek_mut().unwrap(); + if *newest > item { + *newest = item; + } + } + + assert!(oldest_items.len() <= count); + let mut positions = oldest_items.into_iter().map(|item| item.pos).collect_vec(); + positions.sort_unstable_by_key(|&pos| Reverse(pos)); + Ok(EagerRevset { positions }) + } + fn take_latest_revset( &self, candidate_set: &dyn InternalRevset, diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 543e12d1ee..be0aa492a1 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -260,6 +260,10 @@ pub enum RevsetExpression { candidates: Rc, count: usize, }, + Oldest { + candidates: Rc, + count: usize, + }, Filter(RevsetFilterPredicate), /// Marker for subtree that should be intersected as filter. AsFilter(Rc), @@ -376,6 +380,13 @@ impl RevsetExpression { }) } + pub fn oldest(self: &Rc, count: usize) -> Rc { + Rc::new(Self::Oldest { + candidates: self.clone(), + count, + }) + } + /// Commits in `self` that don't have descendants in `self`. pub fn heads(self: &Rc) -> Rc { Rc::new(Self::Heads(self.clone())) @@ -632,6 +643,10 @@ pub enum ResolvedExpression { candidates: Box, count: usize, }, + Oldest { + candidates: Box, + count: usize, + }, Coalesce(Box, Box), Union(Box, Box), /// Intersects `candidates` with `predicate` by filtering. @@ -818,6 +833,16 @@ static BUILTIN_FUNCTION_MAP: Lazy> = Lazy: }; Ok(candidates.latest(count)) }); + map.insert("oldest", |diagnostics, function, context| { + let ([candidates_arg], [count_opt_arg]) = function.expect_arguments()?; + let candidates = lower_expression(diagnostics, candidates_arg, context)?; + let count = if let Some(count_arg) = count_opt_arg { + expect_literal(diagnostics, "integer", count_arg)? + } else { + 1 + }; + Ok(candidates.oldest(count)) + }); map.insert("fork_point", |diagnostics, function, context| { let [expression_arg] = function.expect_exact_arguments()?; let expression = lower_expression(diagnostics, expression_arg, context)?; @@ -1310,6 +1335,11 @@ fn try_transform_expression( candidates, count: *count, }), + RevsetExpression::Oldest { candidates, count } => transform_rec(candidates, pre, post)? + .map(|candidates| RevsetExpression::Oldest { + candidates, + count: *count, + }), RevsetExpression::Filter(_) => None, RevsetExpression::AsFilter(candidates) => { transform_rec(candidates, pre, post)?.map(RevsetExpression::AsFilter) @@ -1504,6 +1534,11 @@ where let count = *count; RevsetExpression::Latest { candidates, count }.into() } + RevsetExpression::Oldest { candidates, count } => { + let candidates = folder.fold_expression(candidates)?; + let count = *count; + RevsetExpression::Oldest { candidates, count }.into() + } RevsetExpression::Filter(predicate) => RevsetExpression::Filter(predicate.clone()).into(), RevsetExpression::AsFilter(candidates) => { let candidates = folder.fold_expression(candidates)?; @@ -2388,6 +2423,10 @@ impl VisibilityResolutionContext<'_> { candidates: self.resolve(candidates).into(), count: *count, }, + RevsetExpression::Oldest { candidates, count } => ResolvedExpression::Oldest { + candidates: self.resolve(candidates).into(), + count: *count, + }, RevsetExpression::Filter(_) | RevsetExpression::AsFilter(_) => { // Top-level filter without intersection: e.g. "~author(_)" is represented as // `AsFilter(NotIn(Filter(Author(_))))`. @@ -2489,7 +2528,8 @@ impl VisibilityResolutionContext<'_> { | RevsetExpression::Heads(_) | RevsetExpression::Roots(_) | RevsetExpression::ForkPoint(_) - | RevsetExpression::Latest { .. } => { + | RevsetExpression::Latest { .. } + | RevsetExpression::Oldest { .. } => { ResolvedPredicateExpression::Set(self.resolve(expression).into()) } RevsetExpression::Filter(predicate) => { @@ -3537,6 +3577,14 @@ mod tests { } "###); + insta::assert_debug_snapshot!( + optimize(parse("oldest(bookmarks() & all(), 2)").unwrap()), @r###" + Oldest { + candidates: CommitRef(Bookmarks(Substring(""))), + count: 2, + } + "###); + insta::assert_debug_snapshot!( optimize(parse("present(foo ~ bar)").unwrap()), @r###" Present(