ok/jj
1
0
Fork 0
forked from mirrors/jj

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 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`). way it looked at an earlier point (`jj op restore`).
When referring to operations, you can use `@` to represent the current operation When referring to operations, you can use `@` to represent the current
as well as the `-` operator (e.g. `@-`) to get the parent of an operation. operation.
The following operators are supported:
* `x-`: Parents of `x` (e.g. `@-`)
* `x+`: Children of `x`
## Concurrent operations ## Concurrent operations

View file

@ -101,22 +101,27 @@ fn resolve_single_op(
get_current_op: impl FnOnce() -> Result<Operation, OpsetEvaluationError>, get_current_op: impl FnOnce() -> Result<Operation, OpsetEvaluationError>,
op_str: &str, op_str: &str,
) -> Result<Operation, OpsetEvaluationError> { ) -> 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 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 { let mut operation = match op_symbol {
"@" => get_current_op(), "@" => get_current_op(),
s => resolve_single_op_from_store(op_store, op_heads_store, s), s => resolve_single_op_from_store(op_store, op_heads_store, s),
}?; }?;
for _ in op_postfix.chars() { for c in op_postfix.chars() {
let mut parent_ops = operation.parents(); let mut neighbor_ops = match c {
let Some(op) = parent_ops.next().transpose()? else { '-' => operation.parents().try_collect()?,
return Err(OpsetResolutionError::EmptyOperations(op_str.to_owned()).into()); '+' => 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) Ok(operation)
} }
@ -178,6 +183,20 @@ fn get_current_head_ops(
.try_collect() .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)] #[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct OperationByEndTime(Operation); struct OperationByEndTime(Operation);

View file

@ -262,7 +262,7 @@ fn test_resolve_op_id() {
} }
#[test] #[test]
fn test_resolve_op_parents() { fn test_resolve_op_parents_children() {
// Use monotonic timestamp to stabilize merge order of transactions // Use monotonic timestamp to stabilize merge order of transactions
let settings = testutils::user_settings(); let settings = testutils::user_settings();
let test_repo = TestRepo::init_with_settings(&settings); let test_repo = TestRepo::init_with_settings(&settings);
@ -275,6 +275,7 @@ fn test_resolve_op_parents() {
operations.push(repo.operation().clone()); operations.push(repo.operation().clone());
} }
// Parent
let op2_id_hex = operations[2].id().hex(); let op2_id_hex = operations[2].id().hex();
assert_eq!( assert_eq!(
op_walk::resolve_op_with_repo(&repo, &format!("{op2_id_hex}-")).unwrap(), 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 tx1 = repo.start_transaction(&settings);
let tx2 = repo.start_transaction(&settings); let tx2 = repo.start_transaction(&settings);
repo = testutils::commit_transactions(&settings, vec![tx1, tx2]); repo = testutils::commit_transactions(&settings, vec![tx1, tx2]);
@ -302,4 +327,11 @@ fn test_resolve_op_parents() {
OpsetResolutionError::MultipleOperations(_) 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(_)
))
);
} }