Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

# I've finished something significant! Carve it out from the working "change" as its own commit.

    git add -p
    git commit
# Oops, missed a piece.

    git add -p
    git commit --amend
# Let me look at what's left.

    git diff
# Oh right, I had started working on something else. I could just leave it in the working change, but let me separate it out into its own commit even though it's unfinished, since I can always add pieces to it later.

    git add -p
    git commit
# Wait, no, I kind of want it to come before that thing I finished up. i didn't mess up, this is standard procedure

    git rebase -i <some older commit ref> # put commits into the desired order.
# I also have some logging code I don't need anymore. Let me discard it.

don't know what jj does here

    edit files
or

    git revert -p
# Do some more work. I have some additions to that part I thought was done.

    git add -p
    git commit
    git rebase -i <some older commit ref> # choose the right place and then tell git to squash into the parent

# And some additions to that other part.

    git add -p
    git commit
    git rebase -i <some older commit ref> # as above
you list a number of different commands that i do in git always with the same sequence of commands. i don't see how jj makes those examples any easier. it looks like maybe they help you get away with not understanding how git works, by instead giving you more commands that do specifically what you want.
 help



Yes, and? I wasn't trying to demonstrate jj superiority. I was responding to a post about a normal messy workflow and showing how to handle it in jj.

If I'm writing a description of how to use jj, I could take several different approaches. Am I writing for a git novice? A git expert? An expert in a different VCS? A novice in any VCS? And even within those, there's a big difference in whether you're a solo dev working alone on their own project, a solo dev working across multiple systems, a random github contributor working alone against a github repo, a group of contributors who work together before landing something in an upstream repo, or whatever. And then, it matters whether my objective is to show that jj is somehow superior, or to just show to accomplish something.

Those are going to require rather different approaches. I was not going for "jj is better than git". I was aiming more for "here's how straightforward it is to do the sort of stuff you're talking about". Even with the example actions I described, jj does have a couple of advantages that I didn't highlight: first, your git equivalents would require looking up commit hashes whereas in jj I tend to remember the recent change ids that I've been working with. Second, `jj undo` (and its stronger variant, `jj op restore`) is easier and simpler to work with than the reflog. A longer example would have demonstrated that, but I didn't want a longer example.

But I have no dispute with your assertion that this workflow is not harder in git. I could write a description of why I think jj is better than git, it's just that my post was not that. (I could also write a post about how jj is still missing some important functionality that git has, and therefore git is better than jj.)

But just to sketch out what I would use if I wanted to make git look bad, I'd probably use an example of multiple independent lines of development where I want to work off of a tree with all of them applied at the same time, without artificially linearizing them because I don't know what order reviews are going to come in, and then doing fixups and incorporating new changes that turn out to conflict, and not getting stuck working through conflicts in the other patch series when I'm actively working out the finishing touches on one of them that turns out to be high priority. And then getting some of that wrong and wanting to back up and try a different path. All while carrying along some temporary logging or debugging changes, and perhaps some configuration changes that I don't ever want pushed. And keeping my patch series clean by separating out refactoring from changes, even when I actually do bits of that refactoring or those changes out of order. And doing all this without risking modifying anything other people might be using or building off of, by preventing force pushes on stuff that matters without preventing it for in-development stuff that is only relevant to me. And in the middle of this, wanting to look back on what the state of things was last Wednesday, including the whole commit graph.

All of that is possible with both git and jujutsu. In practice, I wouldn't even try much of it with git. Perhaps I just suck at git? Very possible. I'm better with mercurial, but I wouldn't do a lot of that there either. I won't say all of that is trivial with jj, but some of it is easy, all of it is doable without thinking too hard, it's the sort of stuff that arises for me quite often as I'm working, and none of it requires more than the same handful of commands and concepts. I know what changes are tentative and what are more fixed without juggling commits vs staging vs stash, and I can freely move bits and pieces between them using the same set of commands. I could do the exact same things in git, but I wouldn't. The core git data model is very nice, so it's pretty clear what can and can't be done. jj gives me the ability to manipulate it without tangling my head or my repo in knots.


i apologize if my comment came across as accusing you of claiming superiority and failing your intention. it was not meant to do that. if there is any accusation then it is the general assumption that jj is better that can be felt in the overall tone of this discussion thread.

your examples show a particular aspect of jj, and to me they demonstrate that jj isn't better across the board for a certain group of users at least.

i'd be interested to know if i got the impression right that jj provides more commands for specific actions that don't require you to understand how the system works underneath. that is significant because i like to understand how things work underneath. more commands then means that i have to learn more to understand each one of them.

in a sense it is like high level languages vs low level languages. don't get me wrong, i like high level languages. i prefer them actually, but i also like minimalistic syntax, so i prefer smalltalk and lisp over, say scala which has a reputation for being complex. but scala is starting to make more sense once i learn what lower level primitives the syntax translates to.

same goes for jj. i want to understand the primitives that the commands translate to. git puts me closer to those primitives, which makes it harder to learn but possibly easier to use or understand once you get it. since jj is built on top of the same primitives it should be possible to reach the same understanding. jj uses those primitives differently, and that's the part that i find interesting. the more i understand how jj works with git primitives the more i like it. but it also shows that git primitives are not bad if they enable such different approaches. (i wonder how different git primitives are from other approaches such as pijul which is based on a theory of patches).

i guess what would help me would be a guide to jj for those who are familiar with how git works underneath.

the examples in your second to last paragraph sound very interesting. please show. of link to references if you know any.


Heh, sorry, I guess you triggered my defensive "book-length response" reaction. Here's the next book:

As you say, jj uses git primives so the core data model is the same. I say "core" because jj subtracts out the staging area and stash and adds in "changes" and the operations log. But otherwise, your understanding of the git data model translates seamlessly, with one exception: git-style branches aren't really a thing, they can only be emulated. What I mean is that in git, the topological head that is associated with a branch ref (apologies if I'm getting terminology wrong), when accessed through a branch name, is taken to represent not just that commit but all ancestors of it as well. So "merging a branch" or "checking out a branch" are natural things to do.

jj prefers to be grounded in the commit DAG. Since you're always "updating" a commit (really replacing it with new ones), the only other state is the location of that commit in the DAG. You specify that before that change exists, with the default being the same as in git: the single descendant of the last thing you were working on (the branch tip in git, the @ change in jj). `jj new` (or `jj commit`) creates a single child change of your current change, unless you pass flags putting it elsewhere in the graph -- and it is very common to put it elsewhere if that's the thing you want to do.

An example: say you have a sequence of changes (and thus commits) where you're working on a feature: A->B->C, with C being what you're editing now (the @ change). You realize that your latest change, C, might not be the best approach and decide to try another. You do `jj new B` to create an alternate descendant and implement a C2 approach. Now you have A->B->{C,C2}. Which of C or C2 is the tip of your "feature branch"? Answer: who cares? At least until you want to push it somewhere. My default log command will show the whole DAG rooted at A, the first change off of "trunk" commits. I might do further work by creating descendants of C2 and thus have A->B->C and B->C2->D->E, and I might rebase or duplicate ->D->E on top of C if I want to try it out with that approach. Or I might inject or modify things earlier in the graph like A->X->B'->Y->{C,C2} (where B' is an updated B). If I push part of that DAG up to a server that I have declared to be shared with other people, then jj will know what parts of the graph should no longer be mutated and prevent me from doing that (by default).

All of this follows the core git model, it's just that mutating the graph structure is expected and common with jj and only restricted modifications seem to be common with git. You can do any and all of it with git, but rebase, undo, and conflicts are better-supported and feel less exceptional with jj. And you can jump to anywhere in the graph, or move around pieces of the graph, anytime and without necessarily changing the @ (working change) you're looking at.

This also means that jj doesn't need a `git switch` or `git checkout` command. That notion is replaced by making your current change (there's always a current change, it's the one that will be associated with any edits you make) be the child of wherever in the DAG you want, probably with `jj new <parent>`. As with git, there's a snapshot associated with it, so the other purpose of `git checkout` (to make your on-disk copy of a file match the contents of a given commit) is `jj restore filename`. In `git help checkout`, it says "Switch branches or restore working tree files". The "switch branches" part is `jj new`, the "restore working tree files" part is `jj restore`. `jj new` is equivalent to `git checkout --detach` or `git checkout <branch> <start-commit>` (or plain `git checkout <treeish>`). If you only consider the "make (part or all of) the working tree match that of this commit", then all of the different variants `git checkout` being in one command makes sense. But jj distinguishes things that operate on the DAG vs trees (file contents). And it doesn't need different flags or modes for the index/stage vs a commit; everything is a commit. `git stash` is unnecessary, the equivalent is `jj new <ancestor>` (you're just moving your working directory to be based on a different commit; the previous patches aren't lost or anything, they stay where they were in the DAG. Grab them from there if you want them and put them anywhere else in the graph, with or without changing your working directory as you wish.) `git reset` is redundant with `jj restore` -- well, mostly; the other part of what it does is handled by `jj abandon`. (Again, `restore` for tree contents, `abandon` for the graph node.) `git merge` is `jj new` with multiple parents instead of one so a command isn't needed.

One way to say it: with jj, `new` + `describe` + `rebase` + `abandon`, you can do all the graph manipulation. Add in `squash` + `restore` and you can do all content manipulation too. That's it for local operation. Well, maybe `jj file track/untrack`. The flags are pretty consistent across commands and don't fundamentally change what they do. Everything else is either for working with servers and other repositories, convenience utilities for specific situations, details like configuration, or jj-specific functionality like the operation log.

The other major difference is that jj adds in the notion of persistent "changes" (I wish we'd come up with a better name) that in normal operation correspond to a single "visible" commit, but which commit that is changes over time. A change has a single semantic purpose and doesn't lose its identity when rebased or merged or the contents of its files are changed. This difference is actually starting to diminish, because git (or rather, the git forges and tooling) has started preserving the footers that identify these change IDs over various operations. So if you're using git in connection with gitbutler or gerrit or whatever, you'll be getting the benefit of these persistent identifiers even if you continue using the git cli.

Sorry, that's a lot, but it'd be my attempt at "jj fundamentals for someone who understands git primitives well".


thank you. the more i read, the more i like it.

removing the staging area and the stash and "emulating" them with real commits for one simplifies the model, but it also upgrades them to first class citizens because you can treat them like every other commit. i think this adds a lot of power to manipulate the history. on the other hand changes and operations log don't add new primitives but are just additional pointers to track the history.

so in summary: less complexity, more tracking, cleaner commands (like restore instead of checkout to fix files)

somewhere a comment stated that git history is more static because it is not so easy to move commits around. i never felt that. i made copious use of rebase and interactive rebase to shove commits around as i liked. in the end i know that a rebase is successful when the diff between the old state and the new state is empty. but you have to understand how git works in order to feel comfortable doing this. it looks like jj makes this easier because it looks like it adds more safeguards. in git for example i would often tag a state before starting a complex operation, despite the reflog so that i could keep track of the changes and easily see what to throw away if the operation failed, so that i could start over. sometimes i redid a rebase two or three times because the outcome was not what i wanted or i messed up somewhere. it was never a big deal because i know how it works. but it was always something that i felt not everyone had the courage to try.

i installed jj last night and i look forward to an opportunity to try it.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: