martinvonz / jj

A Git-compatible VCS that is both simple and powerful
https://martinvonz.github.io/jj/
Apache License 2.0
7.44k stars 244 forks source link

FR: jj undo ergonomics #3700

Open benbrittain opened 1 month ago

benbrittain commented 1 month ago

Is your feature request related to a problem? Please describe. The current behavior of the jj undo command does not seem to be what users intuitively expect. Currently if you run jj undo twice in a row it has the same behavior as jj op undo, it returns you to the same place you were when you started. undo + undo = no change.

Describe the solution you'd like I'd like to preserve the ergonomics of the underlying jj op undo command (as suggested by @PhilipMetzger on Discord), but have the higher level jj undo command track if a user immediately undoes an undo and warn them.

Describe alternatives you've considered Alternatively, the jj undo command could keep track of where you are in the undo log ctrl-Z style. That probably implies a redo command as well?

martinvonz commented 1 month ago

I'd like to preserve the ergonomics of the underlying jj op undo command

Why make them different? Oh, I suppose the top-level jj undo would lose its optional operation argument?

benbrittain commented 1 month ago

I'm actually starting to question if jj op undo is needed at all. I'd originally been modeling it as a lower level operation, but it's really just a special case of jj op restore, no? I can't imagine I'd ever use jj op undo if jj undo had ctrl-z semantics

martinvonz commented 1 month ago

it's really just a special case of jj op restore, no?

No, jj undo @- undoes the second-to-last operation while leaving the changes from the last operation. For example, if you abandoned some commit, and then did a bunch of unrelated changes, you can still recover that commit with jj undo <old operation>. It can be hard to reason about, however, because it also brings back ancestors of the old commit when it makes the old abandoned commit visible.

martinvonz commented 1 month ago

Think of it like jj backout but for operations (while jj op restore is jj restore for operations).

PhilipMetzger commented 1 month ago

To reword the FR a bit:

Improve the ergonomics of jj undo by making it a separate command, which can do undo tracking like editors. Currently running undo twice in a row, surprises a lot of users as it just rolls back the last operation (not surprising if you know that jj undo is an alias of jj op undo). This would then pave a way for a smart redo, which has the opposite behavior.

Describe alternatives you've considered Alternatively, the jj undo command could keep track of where you are in the undo log ctrl-Z style. That probably implies a redo command as well?

I think that this alternative would be layering violation in UI terms, as imo jj op undo shouldn't be aware of such things anyway. That's where the suggestion came from in Discord anyway.

joyously commented 1 month ago

Would this have any impact on #3428 (op log waypoints)?

PhilipMetzger commented 1 month ago

Would this have any impact on #3428 (op log waypoints)?

The higher level jj undo could probably integrate with them, which then simplifies the UX again. op log Waypoints will just be another nice building block for it.

fowles commented 3 weeks ago

I think undo is the wrong idiom to think of these things with. The op log is a continuous stream of states that is append only. Attempts to imagine the current state as a pointer to somewhere in this history of operations is appealing, but in truth you are creating a new state at the head that happens to have identical content to an earlier state.

I would recommend removing undo entirely and instead building on restore as the fundamental unit. Give it good usability so you can say restore <timestamp> and then make it clear that what this command does it creates a new state now that has exactly the same contents as the system had at <timestamp>.

martinvonz commented 3 weeks ago

I would recommend removing undo entirely and instead building on restore as the fundamental unit.

Are you saying that jj op undo <not latest operation> is too hard to understand? I think that's fair. I practically never use it myself.

By the way, I find the suggestion of making jj undo behave differently from jj op undo confusing. That would be resolved by removing jj op undo. Another option is to rename jj op undo to jj op backout (matching jj backout just like jj op restore matches jj restore). Maybe we should start with that rename.

PhilipMetzger commented 3 weeks ago

The op log is a continuous stream of states that is append only. Attempts to imagine the current state as a pointer to somewhere in this history of operations is appealing, but in truth you are creating a new state at the head that happens to have identical content to an earlier state.

I agree with this, but still think that jj undo should be kept with the suggested semantics.

By the way, I find the suggestion of making jj undo behave differently from jj op undo confusing. That would be resolved by removing jj op undo. Another option is to rename jj op undo to jj op backout (matching jj backout just like jj op restore matches jj restore). Maybe we should start with that rename.

That sounds great and should make the implementation simpler as it can just shell out to op restore.

ilyagr commented 2 weeks ago

I have found jj op undo to be occasionally useful, but I like the idea of renaming it to jj op backout.

I'm not 100% sure what jj undo should do if we rename jj op undo to backout until we create a fancy new behavior. It would be a bit sad to get rid of it. I suppose it could be a version of jj op backout that only works on the last operation, and explains to the user that 1) repeating jj undo will undo the undo 2) try jj op log and jj op restore for anything fancy.

I am also not sure what a new and fancy jj undo would do, exactly. I wrote up a long explanation of how (I think) it worked in Emacs, but that made me realize that almost any text-editor-like undo behavior will run into the same problem: what if you do a bunch of undo-s, and then a non-user-initiated operation happens unexpectedly (like snapshotting from jj log running on a timer or because watchman notices something changed)?


Here's the write-up about the old Emacs behavior (or what I think it was)

I'd like to suggest the old Emacs behavior for consideration: if the op log is at

root -> A -> B -> C

the first jj undo undoes C, and the second jj undo undoes A. At that point, the log would be

root -> A -> B -> C -> undo C -> undo B

Now, if you did another undo, it would undo A. If you do any operation D other than undo (or redo), however, the op log becomes

root -> A -> B -> C -> undo C -> undo B -> D

Now, if you start undoing, you will first undo D, then you'll undo "undo B", then you'll undo "undo A", and so on.

The advantage of this is that it works well with an append-only op log, jj op log will tell you exactly what's going on.

There are some disadvantages, most notably: what do we do if "operation D" is an unexpected non-user-initiated operation, such as snapshotting?

PhilipMetzger commented 1 week ago

I am also not sure what a new and fancy jj undo would do, exactly. I wrote up a long explanation of how (I think) it worked in Emacs, but that made me realize that almost any text-editor-like undo behavior will run into the same problem: what if you do a bunch of undo-s, and then a non-user-initiated operation happens unexpectedly (like snapshotting from jj log running on a timer or because watchman notices something changed)?

I think we should be able to fix the Emacs use-case you mentioned with separately tagging the op log transactions to distinguish them from automation and human interaction, like #3428 wants. In my opinion we deserve an actual client/daemon for jj where direct forge integration and a VFS is built in and then having the option to tag where an op came from will be needed.

emilazy commented 1 week ago

Here’s a nice exposition of a completely linear undo feature that never loses history, even as you perform new changes on top of undos; all past states remain accessible at all times: Resolving the Great Undo-Redo Quandary. I don’t know if it would work for Jujutsu, but we should at least consider and internalize its lessons when thinking about this.

(It’s possible Emacs has behaviour similar to this; I’m not sure.)