mirror of
https://github.com/martinvonz/jj.git
synced 2024-12-26 14:00:51 +00:00
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
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(_)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue