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

Add support for external merge tool #18

Closed
martinvonz opened this issue Jun 3, 2021 · 13 comments
Closed

Add support for external merge tool #18

martinvonz opened this issue Jun 3, 2021 · 13 comments
Assignees

Comments

@martinvonz
Copy link
Owner

No description provided.

martinvonz added a commit that referenced this issue Nov 7, 2021
I initially made the working copy materialize conflicts in its
`check_out()` method. Then I changed it later (exactly a year ago, on
Halloween of 2020, actually) so that the working copy expected
conflicts to already have been materalized, which happens in
`MutableRepo::check_out`().

I think my reasoning then was that the file system cannot represent a
conflict. While it's true that the file system itself doesn't have
information to know whether a file represents a conflict, we can
record that ourselves. We already record whether a file is executable
or not and then preserve that if we're on a file system that isn't
able to record it. It's not that different to do the same for
conflicts if we're on a file system that doesn't understand conflicts
(i.e. all file systems).

The plan is to have the working copy remember whether a file
represents a conflict. When we check if it has changed, we parse the
file, including conflict markers, and recreate the conflict from
it. We should be able to do that losslessly (and we should adjust
formats to make it possible if we find cases where it's not).

Having the working copy preserve conflict states has several
advantages:

 * Because conflicts are not materialized in the working copy, you can
   rebase the conflicted commit and the working copy without causing
   more conflicts (that's currently a UX bug I run into every now and
   then).

 * If you don't change anything in the working copy, it will be
   unchanged compared to its parent, which means we'll automatically
   abandon it if you update away from it.

 * The user can choose to resolve only some of the conflicts in a file
   and squash those in, and it'll work they way you'd hope.

 * It should make it easier to implement support for external merge
   tools (#18) without having them treat the working copy differently.

This patch prepares for that work by adding support for parsing
materialized conflicts.
martinvonz added a commit that referenced this issue Nov 7, 2021
I initially made the working copy materialize conflicts in its
`check_out()` method. Then I changed it later (exactly a year ago, on
Halloween of 2020, actually) so that the working copy expected
conflicts to already have been materalized, which happens in
`MutableRepo::check_out`().

I think my reasoning then was that the file system cannot represent a
conflict. While it's true that the file system itself doesn't have
information to know whether a file represents a conflict, we can
record that ourselves. We already record whether a file is executable
or not and then preserve that if we're on a file system that isn't
able to record it. It's not that different to do the same for
conflicts if we're on a file system that doesn't understand conflicts
(i.e. all file systems).

The plan is to have the working copy remember whether a file
represents a conflict. When we check if it has changed, we parse the
file, including conflict markers, and recreate the conflict from
it. We should be able to do that losslessly (and we should adjust
formats to make it possible if we find cases where it's not).

Having the working copy preserve conflict states has several
advantages:

 * Because conflicts are not materialized in the working copy, you can
   rebase the conflicted commit and the working copy without causing
   more conflicts (that's currently a UX bug I run into every now and
   then).

 * If you don't change anything in the working copy, it will be
   unchanged compared to its parent, which means we'll automatically
   abandon it if you update away from it.

 * The user can choose to resolve only some of the conflicts in a file
   and squash those in, and it'll work they way you'd hope.

 * It should make it easier to implement support for external merge
   tools (#18) without having them treat the working copy differently.

This patch prepares for that work by adding support for parsing
materialized conflicts.
martinvonz added a commit that referenced this issue Nov 7, 2021
I initially made the working copy materialize conflicts in its
`check_out()` method. Then I changed it later (exactly a year ago, on
Halloween of 2020, actually) so that the working copy expected
conflicts to already have been materalized, which happens in
`MutableRepo::check_out`().

I think my reasoning then was that the file system cannot represent a
conflict. While it's true that the file system itself doesn't have
information to know whether a file represents a conflict, we can
record that ourselves. We already record whether a file is executable
or not and then preserve that if we're on a file system that isn't
able to record it. It's not that different to do the same for
conflicts if we're on a file system that doesn't understand conflicts
(i.e. all file systems).

The plan is to have the working copy remember whether a file
represents a conflict. When we check if it has changed, we parse the
file, including conflict markers, and recreate the conflict from
it. We should be able to do that losslessly (and we should adjust
formats to make it possible if we find cases where it's not).

Having the working copy preserve conflict states has several
advantages:

 * Because conflicts are not materialized in the working copy, you can
   rebase the conflicted commit and the working copy without causing
   more conflicts (that's currently a UX bug I run into every now and
   then).

 * If you don't change anything in the working copy, it will be
   unchanged compared to its parent, which means we'll automatically
   abandon it if you update away from it.

 * The user can choose to resolve only some of the conflicts in a file
   and squash those in, and it'll work they way you'd hope.

 * It should make it easier to implement support for external merge
   tools (#18) without having them treat the working copy differently.

This patch prepares for that work by adding support for parsing
materialized conflicts.
@EyeCon
Copy link

EyeCon commented Feb 18, 2022

I don't know if this is related, but setting the diff_editor under [ui] (in my case to "vimdiff") as instructed in the tutorial doesn't seem to work:

$ jj split -r 3dac3f00b3ad
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: invalid TOML value, did you mean to use a quoted string? at line 4 column 15 in ../.jjconfig', src/main.rs:23:50
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
$ RUST_BACKTRACE=full jj split -r 3dac3f00b3ad
thread 'main' panicked at 'failed to run diff editor: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/diff_edit.rs:143:10
stack backtrace:
   0:     0x7fe018879fbc - std::backtrace_rs::backtrace::libunwind::trace::he080dbb637fa4641
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/../../backtrace/src/backtrace/libunwind.rs:93:5
   1:     0x7fe018879fbc - std::backtrace_rs::backtrace::trace_unsynchronized::h44a0cce058c40994
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
   2:     0x7fe018879fbc - std::sys_common::backtrace::_print_fmt::hc4aa1034724314cb
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/sys_common/backtrace.rs:66:5
   3:     0x7fe018879fbc - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::h2b790f8be1985180
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/sys_common/backtrace.rs:45:22
   4:     0x7fe0188a4dec - core::fmt::write::h84736921f2ac5f8d
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/core/src/fmt/mod.rs:1190:17
   5:     0x7fe018873a58 - std::io::Write::write_fmt::hfe23991a8d7fa4a7
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/io/mod.rs:1657:15
   6:     0x7fe01887c1e7 - std::sys_common::backtrace::_print::h67116a2d44eae1b1
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/sys_common/backtrace.rs:48:5
   7:     0x7fe01887c1e7 - std::sys_common::backtrace::print::h994f9a0072a4b217
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/sys_common/backtrace.rs:35:9
   8:     0x7fe01887c1e7 - std::panicking::default_hook::{{closure}}::h8f96a84dcab30cf8
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:295:22
   9:     0x7fe01887beaf - std::panicking::default_hook::hb573e2c3be78272d
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:314:9
  10:     0x7fe01887c94a - std::panicking::rust_panic_with_hook::h76b4576d8ac3b270
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:698:17
  11:     0x7fe01887c637 - std::panicking::begin_panic_handler::{{closure}}::h966318ad4399ecd3
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:588:13
  12:     0x7fe01887a464 - std::sys_common::backtrace::__rust_end_short_backtrace::h64a1117353180ced
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/sys_common/backtrace.rs:138:18
  13:     0x7fe01887c339 - rust_begin_unwind
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:584:5
  14:     0x7fe0182cb213 - core::panicking::panic_fmt::h1e67b42cef1db7f9
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/core/src/panicking.rs:143:14
  15:     0x7fe0182cb303 - core::result::unwrap_failed::h1b1ad46cede6e9f5
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/core/src/result.rs:1749:5
  16:     0x7fe01834275c - jujutsu::diff_edit::edit_diff::h11f5f947fdd89247
  17:     0x7fe018306340 - jujutsu::commands::cmd_split::hbe0ab831be98217b
  18:     0x7fe0182cd006 - jujutsu::commands::dispatch::h215fb838a384e92c
  19:     0x7fe0182cddc1 - jj::main::h124478dc1575244a
  20:     0x7fe0182cded3 - std::sys_common::backtrace::__rust_begin_short_backtrace::hec8c8455cb2b9a37
  21:     0x7fe0182cdec9 - std::rt::lang_start::{{closure}}::h11f6c49c99dcd502
  22:     0x7fe0188796a1 - core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once::h9d346f7455a6c7fd
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/core/src/ops/function.rs:259:13
  23:     0x7fe0188796a1 - std::panicking::try::do_call::h8ec1a9b8203a1bbc
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:492:40
  24:     0x7fe0188796a1 - std::panicking::try::h695a82b534df1aab
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:456:19
  25:     0x7fe0188796a1 - std::panic::catch_unwind::h6743ebe0ff045bfe
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panic.rs:137:14
  26:     0x7fe0188796a1 - std::rt::lang_start_internal::{{closure}}::h6e3fc46ec5fb4576
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/rt.rs:128:48
  27:     0x7fe0188796a1 - std::panicking::try::do_call::h5c849f12890c52cc
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:492:40
  28:     0x7fe0188796a1 - std::panicking::try::h2c36151dbc2fb3e1
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panicking.rs:456:19
  29:     0x7fe0188796a1 - std::panic::catch_unwind::h5cca8e7db56bc46a
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/panic.rs:137:14
  30:     0x7fe0188796a1 - std::rt::lang_start_internal::h0e034746b466501e
                               at /rustc/75d9a0ae210dcd078b3985e3550b59064e6603bc/library/std/src/rt.rs:128:20
  31:     0x7fe0182cdf02 - main
  32:     0x7fe017b06d0a - __libc_start_main
  33:     0x7fe0182cba4a - _start
  34:                0x0 - <unknown>
$ cat ~/.jjconfig
[user]
name = [REDACTED]
email = [REDACTED]
diff-editor = "vimdiff"

@ilyagr
Copy link
Collaborator

ilyagr commented Oct 29, 2022

I'd like to add basic mergetool support. I think the best that can be done is to use a mergetool for simple conflicts, A + B - C in jj notation. The command could be jj mergetool FILENAME. It would be configured as follows in config.toml:

merge-tools.meld.merge-args = ["$left", "$base", "$right", "-o", "$output_empty"]
merge-tools.vimdiff.merge-args = ["-f", "-d", "$output_workspace", "-M", "$left", "$base", "$right",
                                  "-c", "wincmd J", "-c", "set modifiable", "-c", "set write"]

jj will replace arguments that start with $ as appropriate. Exactly one of $output_empty or $output_workspace is required. The first of these will ignore the contents of FILENAME in the current commit, and consider the merge successful if anything is written to the file (I think this is the best meld can do). The second will put the current (partially merged) contents of FILENAME at $output_workspace and will consider the merge successful if the file is modified. If there are conflict markers remaining in FILENAME, that is fine as long as they can be parsed.

How does this sound?

@martinvonz
Copy link
Owner Author

Sounds good! Thank you!

I think the best that can be done is to use a mergetool for simple conflicts, A + B - C in jj notation.

Yep, that's definitely good enough to start with. Once that's done, it's probably relatively simple to add support for the the more complex cases by doing one 3-way merge at a time. For example, if the conflict is A + B + C - D - E, we can call the external tool with e.g. $base=D, $left=A, $right=B. If that gets resolved successfully, we replace those three terms with the resolved state.

For non-file states, we'll probably just want ask the user to choose interactively with a prompt. Feel free to leave that, too, for later.

The command could be jj mergetool FILENAME

That matches what git calls it. I like hg's choice better: resolve. What do you think about that?

It would be configured as follows

That configuration matches what @yuja suggested in f6acee4, so good job either digging that up, or extrapolating from current config options :)

Exactly one of $output_empty or $output_workspace is required.

Git and hg seem to have been okay with having a single $output variable. I just tried it with meld and it seems to give you the option to overwrite the output file or leave it unchanged, so it seems it would be fine to pass the workspace file to meld?

consider the merge successful if anything is written to the file

Here's what git does: https://git-scm.com/docs/git-mergetool#Documentation/git-mergetool.txt-mergetoollttoolgttrustExitCode

FYI, a related feature is "partial merge tools", which I added support for to hg in https://www.mercurial-scm.org/repo/hg/rev/f3aafd785e65. It's good to keep that feature in mind, but I don't think it would impact anything. They're invoked quite differently (as a series of filters, not just a single tool), and the same tool is unlikely to work both as a regular merge tool and a partial tool (unlike how difftools and mergetools are often the same binary), which means we probably don't want to share even the configuration.

@ilyagr
Copy link
Collaborator

ilyagr commented Oct 30, 2022

I now have a working but very unfinished version of this at https://github.com/ilyagr/jj/tree/mergetool. For now, you have to recompile to use a different mergetool than Vim. Also, it doesn't function correctly if the output file has any conflict markers in it after the mergetool.

Once that's done, it's probably relatively simple to add support for the the more complex cases by doing one 3-way merge at a time.

That's certainly an option. Another option is to guide the user to the appropriate revisions to solve a conflict -- take them to a revision where the conflict is simple enough, or help them split an octopus merge into a sequence of merges. (This is more difficult for rebases, but perhaps possible with the obslog). Anyway, I'm not planning to address this at the moment.

I like hg's choice better: resolve. What do you think about that?

I agree, resolve is perfect for this.

good job either digging that up, or extrapolating from current config options :)

:). yes, I extrapolated this from the docs for the current configuration. I haven't looked carefully at the current implementation yet, though.

Git and hg seem to have been okay with having a single $output variable.

Yes, let's start with this and see if we ever need to make it more complicated.

consider the merge successful if anything is written to the file.

For now, the merge fails if there is an exit code, if the output file is unchanged, or if it is empty. If we need more logic, we can add it later.

a related feature is "partial merge tools",...

I haven't looked into this in detail, but it looks like, at least for merge tools that let you edit the output file directly, it should be quite simple to add to jj. The output file starts with jj's conflict markers, and after the edit, it can read whatever markers are left. I guess I'm biased since I'm used to vim, which does this, but I'm not sure how many popular merge tools work this way. I'm pretty sure meld doesn't and I am not sure about kdiff3. I can also try Beyond Compare, since their free trial is so generous.

One question that bothers me is what to do if the conflicts cannot be parsed, not to lose the work the user put in. My best idea is to create an original_filename.FAILED_RESOLVE_DELETE_ME file in the revision next to the original file. There's some chance the user will accidentally push it, but it's mitigated by the fact that jj will refuse to push it until the conflict is fixed. Ideally, the following jj resolve command will notice the file, and ask the user whether to delete it or to use it.

@martinvonz
Copy link
Owner Author

Regarding partial merge tools: I'm not sure, but it sounds like you misunderstood what they're about. They're meant to be completely automatic tools for resolving conflicts, even if they can't resolve all conflicts. The builtin resolution of trivial conflicts (where one side is unchanged or both sides changed the same way) can be viewed as a first step in a chain of partial merge tools.

One question that bothers me is what to do if the conflicts cannot be parsed, not to lose the work the user put in.

I think this comment is unrelated to partial merge tools and related to partial conflict resolutions done by the user. Are you thinking of vimdiff-like tools that expect an output file that has been pre-populated with conflict markers?

Let's say the conflict looked like this (when rendered as conflict markers):

line 1
line 2
<<<<<<<
+++++++
line 3a
%%%%%%%
-line 3
+line 3b
>>>>>>>
line 4
line 5

Maybe the user removed the <<<<<<< and left the file like that. If the user made their merge tool exit with status 0, I think we should just trust that and not try to parse any conflict markers. We could print a warning if we find any <<<<<<<, %%%%%%%, etc., however.

By the way, should we use the usual 3-way marker style when we pre-populate the output file? I don't know what vimdiff and similar tools use the pre-populated contents for. If they interpret them in some way, they're surely more likely to be able to interpret the usual 3-way markers.

@ilyagr
Copy link
Collaborator

ilyagr commented Oct 31, 2022

Indeed, I was talking solely about partial conflict resolution.

The initial version of my code did exactly what you suggest: it took whatever the merge tool output was, and put it into the repo as a regular file. It was not very pleasant -- jj has trouble with such files. For instance, jj diff had trouble displaying anything reasonable for them. It should be easy to revert my code to this behavior, but it seems barely usable. It would make more sense if the output file started out empty, which seems to not make any difference to tools other than vimdiff, but I like being able to resolve conflicts in a terminal...

I now have two approaches: abort if there seem to be any conflicts in the file or use existing functions to parse the file as a conflict. I haven't had time to write any tests or figure out what the latter versions would even do in your example.

By the way, should we use the usual 3-way marker style when we pre-populate the output file?

This could be an option, if we find any tools that benefit from that. vimdiff certainly works with jj's style, though I haven't checked whether it works better with git's style. meld or kdiff3 ignore the output file contents entirely.

It would be very nice to support conflict resolution in VS Code, but I'm not sure if it is possible to use VS Code as a mergetool without writing a VS Code extension. It would certainly be possible for repos with --git-dir . if jj simulated git's merge states, but that's a whole different problem. I only know that merge states use git's staging area in some bizarre fashion.

@martinvonz
Copy link
Owner Author

It was not very pleasant -- jj has trouble with such files. For instance, jj diff had trouble displaying anything reasonable for them.

Ah, you mean if the file gets replaced by a regular file with conflict markers in? FYI, what jj diff does when one side is a conflict is to render it as conflict markers, so if you show a diff where the left side was a conflict and the right side is a regular file with the same conflict markers (because the user accidentally marked it as resolved), then it's going to be an empty diff, except for the type transition, so it's going to look like this:

Resolved conflict in some/path:
    ...

I agree that that seems confusing. One option is to render conflicts as empty strings in the pre-populated output file. I just tried vimdiff to get a better understanding for how it works. It seems it doesn't actually help you merge at all. So I guess that's why it's called vimdiff and not vimmerge :) For primitive tools like that, empty strings does seem to make sense (and actual merge tools probably don't care about the initial contents, so they're also fine with it). What do you think? Another option is to simply use the base file as initial output.

FYI, another option for making the jj diff output less confusing in these cases would be to use a format closer to Git's "combined diff" format (git diff -c/--cc). Something like this for conflict B + C - A resolved as D, i.e. diff D - (B + C - A) = D - B - C + A:

- b
+ d
 -c
 +a

That makes logical sense, but I think the diff against rendered conflict markers are generally easier to read. We could still add support for this "combined diff" as an option. If we do, we could hint about it when it looks like there are still conflict markers in the resolved side of diff with a conflict.

abort if there seem to be any conflicts in the file

That would mean that you cannot use the merge tool to resolve a conflict if the result is actually supposed to have conflict markers in it (like some of our test files).

use existing functions to parse the file as a conflict

I think we should trust the merge tool instead. Hopefully the idea about render conflict hunks as empty strings will work.

@martinvonz
Copy link
Owner Author

Oh, another option for diffs of conflicts would be to show a header near each resolved conflict, maybe right after where we currently show ... for skipped context lines. Then incorrectly resolved hunks would appear as removing a conflict marker and adding the same (or similar) conflict marker.

@ilyagr
Copy link
Collaborator

ilyagr commented Nov 1, 2022

One option is to render conflicts as empty strings in the pre-populated output file. I just tried vimdiff to get a better understanding for how it works. It seems it doesn't actually help you merge at all. .....

I find it very helpful simply to see the three versions of the file in three separate panes. The difftool functionality I use in vimdiff is the fact that all the panes scroll simultaneously, the folding, and the ]c and [c commands to navigate between changes. It is primitive, but I often find it much nicer than editing the conflict markers directly, and these are the only two options I know for conflict resolution in the terminal.

For primitive tools like that, empty strings does seem to make sense (....). What do you think? Another option is to simply use the base file as initial output.

I don't like this idea. If I had to choose, I like the idea of empty strings better than using the base file (which I can get from the base file window anyway). I think vimdiff would still be hard to use with that. It would make more sense if vimdiff was a more feature-full merge tool and highlighted the differences between the output window and the left/right version better than it currently does.

A more minor reason to dislike these two options is that I like the possibility of partial conflict resolution. I haven't yet had a conflict complicated enough to make that necessary, but it seems likely to happen eventually.

None of the graphical (AKA non-primitive) merge tools I tried so far seem to care about what's in the output file initially.

I think we should trust the merge tool instead. Hopefully the idea about render conflict hunks as empty strings will work.

I think that the simplest behavior that makes sense is to default to starting with an empty file for $output by default, but having an option to switch to outputting the file with conflict markers and then parsing the conflict markers.

For now, #689 only implements the latter behavior. For me, at least, it seems to work well. However, if that continues to be the default behavior, there is potential for some merge tool I don't know or somebody's custom script to get confused.

Ah, you mean if the file gets replaced by a regular file with conflict markers in? .... then it's going to be an empty diff, except for the type transition

That's right, and the diff you described is what I saw.

FYI, another option for making the jj diff output less confusing in these cases would be to use a format closer to Git's "combined diff" format

I don't understand the various diff formats git uses for conflicts well enough to comment about that. I would not be surprised if they are very useful in some cases.

another option for diffs of conflicts would be to show a header near each resolved conflict

I like this idea. Currently, I resolve a conflict in a merge commit (for example), I find the diff jj shows for that merge commit quite difficult to understand (the coloring helps a lot, I'm completely lost without it).

However, there are some difficulties. We run into the problem of what to do when a file or a conflict includes text that looks like a header. I guess we don't absolutely have to require the normal diff output to be able to represent everything. We could instead have an option to output Git-style conflict resolution diffs as you discussed. I assume those are designed to handle all cases.

Aside: I also find it weirdly difficult to see what the conflict is. This is the only case in which I need to switch to the commit and use jj status. In all other cases, I can find out everything important about a commit with jj log -r and jj diff -r commands. I'm not sure what a good solution to this would be, though. Perhaps there could be a jj resolve list command, or the commands could be jj conflict resolve and jj conflict list.

@ilyagr
Copy link
Collaborator

ilyagr commented Nov 1, 2022

BTW, I tried out the case of the user messing up the conflict markers you mentioned in #18 (comment). Arguably, that works out OK -- the messed up conflict is considered "resolved" text, and no information is lost.

I found one problem: if the messed up conflict in the only conflict in the file, the entire file is considered resolved. I think this can only be undone with jj obslog + jj restore or jj op restore. I'm not sure if there's an easy fix, but perhaps this is not a huge problem.

@martinvonz
Copy link
Owner Author

One option is to render conflicts as empty strings in the pre-populated output file. I just tried vimdiff to get a better understanding for how it works. It seems it doesn't actually help you merge at all. .....

I find it very helpful simply to see the three versions of the file in three separate panes. The difftool functionality I use in vimdiff is the fact that all the panes scroll simultaneously, the folding, and the ]c and [c commands to navigate between changes. It is primitive, but I often find it much nicer than editing the conflict markers directly, and these are the only two options I know for conflict resolution in the terminal.

Sorry, I understand that vimdiff helps you merge by presenting you with the diffs. What I meant is that it doesn't actually do any merging - it doesn't even resolve trivial conflicts. So even if a hunk changed only on one side, vimdiff will not help you by putting the changed side in the output file (because it's a diff tool, not a merge tool). That's why the VCS needs to help it by merging as much as possible before handing control to vimdiff.

For primitive tools like that, empty strings does seem to make sense (....). What do you think? Another option is to simply use the base file as initial output.

I don't like this idea. If I had to choose, I like the idea of empty strings better than using the base file (which I can get from the base file window anyway). I think vimdiff would still be hard to use with that. It would make more sense if vimdiff was a more feature-full merge tool and highlighted the differences between the output window and the left/right version better than it currently does.

Makes sense.

A more minor reason to dislike these two options is that I like the possibility of partial conflict resolution. I haven't yet had a conflict complicated enough to make that necessary, but it seems likely to happen eventually.

We may want a per-tool option for that, but that can come later.

I think we should trust the merge tool instead. Hopefully the idea about render conflict hunks as empty strings will work.

I think that the simplest behavior that makes sense is to default to starting with an empty file for $output by default, but having an option to switch to outputting the file with conflict markers and then parsing the conflict markers.

Yes. Now that I understand better what vimdiff is, the "premerge" config in Mercurial finally makes more sense. That's exactly what you're suggesting, I think. I agree that we should copy that feature.

FYI, another option for making the jj diff output less confusing in these cases would be to use a format closer to Git's "combined diff" format

I don't understand the various diff formats git uses for conflicts well enough to comment about that. I would not be surprised if they are very useful in some cases.

FWIW, Git has only very recently gained an option to show a diff very similar to ours.

another option for diffs of conflicts would be to show a header near each resolved conflict

I like this idea. Currently, I resolve a conflict in a merge commit (for example), I find the diff jj shows for that merge commit quite difficult to understand (the coloring helps a lot, I'm completely lost without it).

I doubt I'll prioritize implementing that format soon, but it's probably not very hard.

However, there are some difficulties. We run into the problem of what to do when a file or a conflict includes text that looks like a header. I guess we don't absolutely have to require the normal diff output to be able to represent everything. We could instead have an option to output Git-style conflict resolution diffs as you discussed. I assume those are designed to handle all cases.

Since the diff contents in our jj diff format are indented, I don't think that will be a problem.

Aside: I also find it weirdly difficult to see what the conflict is. This is the only case in which I need to switch to the commit and use jj status. In all other cases, I can find out everything important about a commit with jj log -r and jj diff -r commands. I'm not sure what a good solution to this would be, though. Perhaps there could be a jj resolve list command, or the commands could be jj conflict resolve and jj conflict list.

Good point, we may want to split it up like that.

I found one problem: if the messed up conflict in the only conflict in the file, the entire file is considered resolved. I think this can only be undone with jj obslog + jj restore or jj op restore. I'm not sure if there's an easy fix, but perhaps this is not a huge problem.

You shouldn't need jj op restore. If the conflict was also in the parent commit, you can restore it from there (jj restore --from @- some/path). Otherwise you can restore it using the obslog, as you found.

@martinvonz
Copy link
Owner Author

@ilyagr, do you think we should close this now? I think what you implemented in #689 is enough to let us say that we support external merge tools. I know you're already adding more features, so you decide if you want to finish those first.

@ilyagr
Copy link
Collaborator

ilyagr commented Dec 4, 2022

I'll close this one. Some of the TODOs in the code may or may not become new bugs.

@ilyagr ilyagr closed this as completed Dec 4, 2022
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

3 participants