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:
parent
ab299a6af5
commit
3eafca65ea
3 changed files with 69 additions and 13 deletions
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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(_)
|
||||||
|
))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue