Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(debug) assertion failure at MergedTree::merge() by rebasing resolvable commit #2654

Closed
yuja opened this issue Dec 1, 2023 · 4 comments
Closed

Comments

@yuja
Copy link
Collaborator

yuja commented Dec 1, 2023

Description

Maybe this is another case of 7fda80f? Without debug assertion, we'll get a conflicted file with no conflict markers.

Steps to Reproduce the Problem

rm -Rf repo
jj init --git repo
cd repo

# create conflict in subtree 'x'
jj new -mR 'root()'
mkdir x
echo R > x/file

jj new -mA 'description(R)'
echo A >> x/file

jj new -mB 'description(R)'
echo B >> x/file

jj rebase -r 'description(A)' -d 'description(B)'
jj log --no-pager
jj resolve -lr 'description(A)'

# add deletion to conflict (which I don't know if differs from adding more conflicts)
jj rebase -r 'description(A)' -d 'root()'
jj log --no-pager
jj resolve -lr 'description(A)'

# new base to which the conflicted commit can be merged cleanly
jj new -mC 'root()'
mkdir x
cat <<'EOF' > x/file
C
R
EOF

# rebase onto it
jj rebase -r 'description(A)' -d 'description(C)'
jj log --no-pager

Expected Behavior

The last jj rebase succeeds.

Actual Behavior

+ jj rebase -r description(A) -d description(C)
thread 'main' panicked at lib/src/merged_tree.rs:401:17:
assertion `left == right` failed
  left: Resolved(Tree { dir: "", id: TreeId("1ad9dd045be836edfd3844740ce51c475cd39ef6") })
 right: Conflicted([Tree { dir: "", id: TreeId("a0a6a5e7cc7ecd3a80c5331268d057c4b69dc390") }, Tree { dir: "", id: TreeId("e54e2235e3ca7ad9718d23af1d4e7cc663f626a8") }, Tree { dir: "", id: TreeId("0995303dc5e81edeba1c9e48b305d22219b925f0") }])
stack backtrace:
   0: rust_begin_unwind
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/std/src/panicking.rs:597:5
   1: core::panicking::panic_fmt
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/panicking.rs:72:14
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/panicking.rs:270:5
   4: jj_lib::merged_tree::MergedTree::merge
             at /home/yuya/work/2023/jj/lib/src/merged_tree.rs:401:17
   5: jj_lib::rewrite::rebase_commit_with_options
             at /home/yuya/work/2023/jj/lib/src/rewrite.rs:147:13
   6: jj_lib::rewrite::rebase_commit
             at /home/yuya/work/2023/jj/lib/src/rewrite.rs:107:5
   7: jj_cli::commands::rebase::rebase_revision
             at /home/yuya/work/2023/jj/cli/src/commands/rebase.rs:423:5
   8: jj_cli::commands::rebase::cmd_rebase
             at /home/yuya/work/2023/jj/cli/src/commands/rebase.rs:197:9
   9: jj_cli::commands::run_command
             at /home/yuya/work/2023/jj/cli/src/commands/mod.rs:187:39
  10: core::ops::function::FnOnce::call_once
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/ops/function.rs:250:5
  11: core::ops::function::FnOnce::call_once{{vtable.shim}}
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/ops/function.rs:250:5
  12: <alloc::boxed::Box<F,A> as core::ops::function::FnOnce<Args>>::call_once
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/alloc/src/boxed.rs:2007:9
  13: jj_cli::cli_util::CliRunner::run_internal
             at /home/yuya/work/2023/jj/cli/src/cli_util.rs:2876:9
  14: jj_cli::cli_util::CliRunner::run
             at /home/yuya/work/2023/jj/cli/src/cli_util.rs:2893:22
  15: jj::main
             at /home/yuya/work/2023/jj/cli/src/main.rs:18:5
  16: core::ops::function::FnOnce::call_once
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/ops/function.rs:250:5

Specifications

  • Platform: Linux
  • Version: 0.11.0-ec7f9e79f67d9a5a12d221c60a91d59b5eb6b925 (debug assertion enabled)
@martinvonz
Copy link
Owner

Thanks for reporting. I don't know what's going on there yet. It bothers me, but the bug is quite harmless, so I'll probably prioritize more impactful changes in the near future.

@yuja
Copy link
Collaborator Author

yuja commented Dec 2, 2023

I think the problem is that the conflicted tree may be padded with absent trees, whereas file-level conflict resolution requires that all tree values are files.

jj/lib/src/tree.rs

Lines 399 to 460 in 48d586c

pub fn try_resolve_file_conflict(
store: &Store,
filename: &RepoPath,
conflict: &MergedTreeValue,
) -> Result<Option<TreeValue>, TreeMergeError> {
// If there are any non-file or any missing parts in the conflict, we can't
// merge it. We check early so we don't waste time reading file contents if
// we can't merge them anyway. At the same time we determine whether the
// resulting file should be executable.
let Some(file_id_conflict) = conflict.maybe_map(|term| match term {
Some(TreeValue::File { id, executable: _ }) => Some(id),
_ => None,
}) else {
return Ok(None);
};
let Some(executable_conflict) = conflict.maybe_map(|term| match term {
Some(TreeValue::File { id: _, executable }) => Some(executable),
_ => None,
}) else {
return Ok(None);
};
let Some(&&executable) = executable_conflict.resolve_trivial() else {
// We're unable to determine whether the result should be executable
return Ok(None);
};
if let Some(&resolved_file_id) = file_id_conflict.resolve_trivial() {
// Don't bother reading the file contents if the conflict can be trivially
// resolved.
return Ok(Some(TreeValue::File {
id: resolved_file_id.clone(),
executable,
}));
}
// Simplify the conflict for two reasons:
// 1. Avoid reading unchanged file contents
// 2. The simplified conflict can sometimes be resolved when the unsimplfied one
// cannot
let file_id_conflict = file_id_conflict.simplify();
let contents: Merge<Vec<u8>> =
file_id_conflict.try_map(|&file_id| -> Result<Vec<u8>, TreeMergeError> {
let mut content = vec![];
store
.read_file(filename, file_id)?
.read_to_end(&mut content)
.map_err(|err| TreeMergeError::ReadError {
source: err,
file_id: file_id.clone(),
})?;
Ok(content)
})?;
let slices = contents.map(|content| content.as_slice());
let merge_result = files::merge(&slices);
match merge_result {
MergeResult::Resolved(merged_content) => {
let id = store.write_file(filename, &mut merged_content.0.as_slice())?;
Ok(Some(TreeValue::File { id, executable }))
}
MergeResult::Conflict(_) => Ok(None),
}
}

Inserting .simplify() somewhere before the file_id_conflict extraction appears to be fixed the problem.

@martinvonz
Copy link
Owner

Oh, that makes sense!

@yuja
Copy link
Collaborator Author

yuja commented Dec 2, 2023

Hmm, it might be because an empty subtree is added to the parent while propagating conflicts up, creating non-empty parent tree containing empty trees. I'll take a look.

[UPDATED] or maybe there are two separate problems. Adding empty subtree to parent is probably wrong, but merge_tree_values() should be able to handle absent value represented by None.

yuja added a commit to yuja/jj that referenced this issue Dec 2, 2023
Otherwise an empty subtree would be added to the parent tree.

If the stored tree contained an empty subtree, simplify() wouldn't work
against new "absent" subtree representation. I don't know if there's a
such code path, but I believe it's very rare to encounter the problem.

martinvonz#2654
yuja added a commit to yuja/jj that referenced this issue Dec 2, 2023
…flict

I think this is a variant of the problem fixed by 7fda80f "tree: simplify
conflict before resolving at hunk level." We need to simplify() the conflict
before and after extracting file ids because the source conflict values may
contain trees to be cancelled out, and the file values may differ only in exec
bits. Since the legacy tree passes a simplified conflict in to this function,
I made the merged tree do the same.

Fixes martinvonz#2654
yuja added a commit that referenced this issue Dec 2, 2023
Otherwise an empty subtree would be added to the parent tree.

If the stored tree contained an empty subtree, simplify() wouldn't work
against new "absent" subtree representation. I don't know if there's a
such code path, but I believe it's very rare to encounter the problem.

#2654
@yuja yuja closed this as completed in 35f718f Dec 2, 2023
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this issue Dec 11, 2023
## [0.12.0] - 2023-12-05

### Breaking changes

* The `remote_branches()` revset no longer includes branches exported to the Git
  repository (so called Git-tracking branches.)

* `jj branch set` no longer creates a new branch. Use `jj branch create`
  instead.

* `jj init --git` in an existing Git repository now errors and exits rather than
  creating a second Git store.

### New features

* `jj workspace add` can now take _multiple_ `--revision` arguments, which will
  create a new workspace with its working-copy commit on top of all the parents,
  as if you had run `jj new r1 r2 r3 ...`.

* You can now set `git.abandon-unreachable-commits = false` to disable the
  usual behavior where commits that became unreachable in the Git repo are
  abandoned ([#2504](martinvonz/jj#2504)).

* `jj new` gained a `--no-edit` option to prevent editing the newly created
  commit. For example, `jj new a b --no-edit -m Merge` creates a merge commit
  without affecting the working copy.

* `jj rebase` now takes the flag `--skip-empty`, which doesn't copy over commits
  that would become empty after a rebase.

* There is a new `jj util gc` command for cleaning up the repository storage.
  For now, it simply runs `git gc` on the backing Git repo (when using the Git
  backend).

### Fixed bugs

* Fixed another file conflict resolution issue where `jj status` would disagree
  with the actual file content.
  [#2654](martinvonz/jj#2654)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants