Skip to content

Commit

Permalink
Feature: Introduce oldest() to the revset language to return the oldest
Browse files Browse the repository at this point in the history
commits in a set.
  • Loading branch information
AjithPanneerselvam committed Feb 4, 2025
1 parent d620ef9 commit ebec07f
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/revsets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions lib/src/default_index/revset_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -1057,6 +1061,51 @@ impl EvaluationContext<'_> {
Ok(EagerRevset { positions })
}

fn take_oldest_revset(
&self,
candidate_set: &dyn InternalRevset,
count: usize,
) -> Result<EagerRevset, RevsetEvaluationError> {
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,
Expand Down
50 changes: 49 additions & 1 deletion lib/src/revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,10 @@ pub enum RevsetExpression<St: ExpressionState> {
candidates: Rc<Self>,
count: usize,
},
Oldest {
candidates: Rc<Self>,
count: usize,
},
Filter(RevsetFilterPredicate),
/// Marker for subtree that should be intersected as filter.
AsFilter(Rc<Self>),
Expand Down Expand Up @@ -376,6 +380,13 @@ impl<St: ExpressionState> RevsetExpression<St> {
})
}

pub fn oldest(self: &Rc<Self>, count: usize) -> Rc<Self> {
Rc::new(Self::Oldest {
candidates: self.clone(),
count,
})
}

/// Commits in `self` that don't have descendants in `self`.
pub fn heads(self: &Rc<Self>) -> Rc<Self> {
Rc::new(Self::Heads(self.clone()))
Expand Down Expand Up @@ -632,6 +643,10 @@ pub enum ResolvedExpression {
candidates: Box<Self>,
count: usize,
},
Oldest {
candidates: Box<Self>,
count: usize,
},
Coalesce(Box<Self>, Box<Self>),
Union(Box<Self>, Box<Self>),
/// Intersects `candidates` with `predicate` by filtering.
Expand Down Expand Up @@ -818,6 +833,16 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = 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)?;
Expand Down Expand Up @@ -1310,6 +1335,11 @@ fn try_transform_expression<St: ExpressionState, E>(
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)
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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(_))))`.
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit ebec07f

Please sign in to comment.