op_walk: add support for op_id+ (children) operator

A possible use case is when doing some archaeology around a certain operation.

The current implementation is quadratic if + is repeated. Suppose op_id is
usually close to the current op heads, I think it'll practically work better
than building a reverse lookup table.
This commit is contained in:
Yuya Nishihara 2023-12-31 14:58:52 +09:00
parent ab299a6af5
commit 3eafca65ea
3 changed files with 69 additions and 13 deletions

View file

@ -17,8 +17,13 @@ The operation log allows you to undo an operation (`jj [op] undo`), which doesn'
need to be the most recent one. It also lets you restore the entire repo to the
way it looked at an earlier point (`jj op restore`).
When referring to operations, you can use `@` to represent the current operation
as well as the `-` operator (e.g. `@-`) to get the parent of an operation.
When referring to operations, you can use `@` to represent the current
operation.
The following operators are supported:
* `x-`: Parents of `x` (e.g. `@-`)
* `x+`: Children of `x`
## Concurrent operations

View file

@ -101,22 +101,27 @@ fn resolve_single_op(
get_current_op: impl FnOnce() -> Result<Operation, OpsetEvaluationError>,
op_str: &str,
) -> Result<Operation, OpsetEvaluationError> {
let op_symbol = op_str.trim_end_matches('-');
let op_symbol = op_str.trim_end_matches(['-', '+']);
let op_postfix = &op_str[op_symbol.len()..];
let head_ops = op_postfix
.contains('+')
.then(|| get_current_head_ops(op_store, op_heads_store))
.transpose()?;
let mut operation = match op_symbol {
"@" => get_current_op(),
s => resolve_single_op_from_store(op_store, op_heads_store, s),
}?;
for _ in op_postfix.chars() {
let mut parent_ops = operation.parents();
let Some(op) = parent_ops.next().transpose()? else {
return Err(OpsetResolutionError::EmptyOperations(op_str.to_owned()).into());
for c in op_postfix.chars() {
let mut neighbor_ops = match c {
'-' => operation.parents().try_collect()?,
'+' => find_child_ops(head_ops.as_ref().unwrap(), operation.id())?,
_ => unreachable!(),
};
operation = match neighbor_ops.len() {
0 => Err(OpsetResolutionError::EmptyOperations(op_str.to_owned()))?,
1 => neighbor_ops.pop().unwrap(),
_ => Err(OpsetResolutionError::MultipleOperations(op_str.to_owned()))?,
};
if parent_ops.next().is_some() {
return Err(OpsetResolutionError::MultipleOperations(op_str.to_owned()).into());
}
drop(parent_ops);
operation = op;
}
Ok(operation)
}
@ -178,6 +183,20 @@ fn get_current_head_ops(
.try_collect()
}
/// Looks up children of the `root_op_id` by traversing from the `head_ops`.
///
/// This will be slow if the `root_op_id` is far away (or unreachable) from the
/// `head_ops`.
fn find_child_ops(
head_ops: &[Operation],
root_op_id: &OperationId,
) -> OpStoreResult<Vec<Operation>> {
walk_ancestors(head_ops)
.take_while(|res| res.as_ref().map_or(true, |op| op.id() != root_op_id))
.filter_ok(|op| op.parent_ids().iter().any(|id| id == root_op_id))
.try_collect()
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct OperationByEndTime(Operation);

View file

@ -262,7 +262,7 @@ fn test_resolve_op_id() {
}
#[test]
fn test_resolve_op_parents() {
fn test_resolve_op_parents_children() {
// Use monotonic timestamp to stabilize merge order of transactions
let settings = testutils::user_settings();
let test_repo = TestRepo::init_with_settings(&settings);
@ -275,6 +275,7 @@ fn test_resolve_op_parents() {
operations.push(repo.operation().clone());
}
// Parent
let op2_id_hex = operations[2].id().hex();
assert_eq!(
op_walk::resolve_op_with_repo(&repo, &format!("{op2_id_hex}-")).unwrap(),
@ -292,6 +293,30 @@ fn test_resolve_op_parents() {
))
);
// Child
let op0_id_hex = operations[0].id().hex();
assert_eq!(
op_walk::resolve_op_with_repo(&repo, &format!("{op0_id_hex}+")).unwrap(),
operations[1]
);
assert_eq!(
op_walk::resolve_op_with_repo(&repo, &format!("{op0_id_hex}++")).unwrap(),
operations[2]
);
assert_matches!(
op_walk::resolve_op_with_repo(&repo, &format!("{op0_id_hex}+++")),
Err(OpsetEvaluationError::OpsetResolution(
OpsetResolutionError::EmptyOperations(_)
))
);
// Child of parent
assert_eq!(
op_walk::resolve_op_with_repo(&repo, &format!("{op2_id_hex}--+")).unwrap(),
operations[1]
);
// Merge and fork
let tx1 = repo.start_transaction(&settings);
let tx2 = repo.start_transaction(&settings);
repo = testutils::commit_transactions(&settings, vec![tx1, tx2]);
@ -302,4 +327,11 @@ fn test_resolve_op_parents() {
OpsetResolutionError::MultipleOperations(_)
))
);
let op2_id_hex = operations[2].id().hex();
assert_matches!(
op_walk::resolve_op_with_repo(&repo, &format!("{op2_id_hex}+")),
Err(OpsetEvaluationError::OpsetResolution(
OpsetResolutionError::MultipleOperations(_)
))
);
}