This is an amusing anecdote. But the only lesson to be learned is to commit early, commit often.
This is an amusing anecdote. But the only lesson to be learned is to commit early, commit often.
(Side ask to people using Jujutsu: isn't it a use case where jujutsu shines?)
But why are any PRs like this? Each PR should represent an atomic action against the codebase - implementing feature 1234, fixing bug 4567. The project's changelog should only be updated at the end of each PR. The fact that I went down the wrong path three times doesn't need to be documented.
We can bikeshed about this for days. Not every feature can be made in an atomic way.
To be honest, I usually get this with people who have never realized that you can merge dead code (code that is never called). You can basically merge an entire feature this way, with the last PR “turning it on” or adding a feature flag — optionally removing the old code at this point as well.
My industry is also fairly strictly regulated and we plainly cannot do that even if we wanted to, but that's admittedly a niche case.
No more than normal? Generally speaking, the author working on the feature is the only one who’s working on the new code, right? The whole team can see it, but generally isn’t using it.
> If the code is being changed for another reason, or the new feature needs to update code used in many places, etc. It can be much more practical to just have a long-lived branch, merge changes from upstream yourself, and merge when it's ready.
If you have people good at what they do ... maybe. I’ve seen this end very badly due to merge artefacts, so I wouldn’t recommend doing any merges, but rebasing instead. In any case, you can always copy a function to another function: do_something_v2(). Then after you remove the v1, remove the v2 prefix. It isn’t rocket science.
> My industry is also fairly strictly regulated and we plainly cannot do that even if we wanted to, but that's admittedly a niche case.
I can’t think of any regulations in any country (and I know of a lot of them) that dictate how you do code changes. The only thing I can think of is your own company’s policies in relation to those regulations; in which case, you can change your own policies.
> I can’t think of any regulations in any country (and I know of a lot of them) that dictate how you do code changes
https://blog.johner-institute.com/regulatory-affairs/design-...
Hey look at us, two alike thinking people! I never said "let's include all the mess".
Looking at the other extreme someone in this thread said they didn't want other people to see the 3 attempts it took to get it right. Sure if it's just a mess (or, since this is 2025, ai slop) squash it away. But in some situations you want to keep a history of the failed attemps. Maybe one of them was actually the better solution but you were just short of making it work, or maybe someone in the future will be able to see that method X didn't work and won't have to find out himself.
Main should be a clear, concise log of changes. It's already hard enough to parse code and it's made even harder by then parsing versions throughout the code's history, we should try to minimize the cognitive load required to track the number of times something is added and then immediately removed because there's going to be enough of that already in the finished merges.
You already have the information in a commit. Moving that to another database like a wiki or markdown file is work and it is lossy. If you create branches to archive history you end up with branches that stick around indefinitely which I think most would feel is worse.
> Main should be a clear, concise log of changes.
No, that's what a changelog is for.
You can already view a range of commits as one diff in git. You don't need to squash them in the history to do that.
I am beginning to think that the people who advocate for squashing everything have `git commit` bound to ctrl+s and smash that every couple minutes with an auto-generated commit message. The characterization that commits are necessarily messy and need to be squashed as to "minimize the cognitive load" is just not my experience.
Nobody who advocates for squashing even talks about how they reason about squashing the commit messages. Like it doesn't come into their calculation. Why is that? My guess is, they don't write commit messages. And that's a big reason why they think that commits have high "cognitive load".
Some of my commit messages are longer than the code diffs. Other times, the code diffs are substantial and there are is a paragraph or three explaining it in the commit message.
Having to squash commits with paragraphs of commit messages always loses resolution and specificity. It removes context and creates more work for me to try to figure out how to squash it in a way where the messages can be understood with the context removed by the squash. I don't know why you would do that to yourself?
If you have a totally different workflow where your commits are not deliberate, then maybe squashing every merge as a matter of policy makes sense there. But don't advocate that as a general rule for everyone.
It seems some people treat every commit like it's its own little tidy PR, when others do not. For me, a commit is a place to save my WIP when I'm context switching, or to create a save point when I know my code works so that I can revert back to that if something goes awry during refactoring, it's a step on the way to completing my task. The PR is the final product to be reviewed, it's where you get the explanation. The commits are imperfect steps along the way.
For others, every commit is the equivalent of a PR. To me that doesn't make a lot of sense - now the PR isn't an (ideal world) atomic update leading to a single goal, it's a digest of changes, some of which require paragraphs of explanation to understand the reasoning behind. What happens if you realize that your last commit was the incorrect approach? Are you constantly rebasing? Is that the reader's problem? Sure, that happens with PRs as well, but again, that's the difference in process - raising a PR requires a much higher standard of completion than a commit.
Most of the time, I don't have a clean path through a piece of work such that I can split it out into beautifully concise commits with perfect commit messages. I have WIP commits, messy changes, bad variable names, mistakes, corrections, real corrections, things that I expect everyone does. I commit them every one of them. This is my "private history" or my scratch work.
After I've gotten to the end and I'm satisfied that I've proven my change does what its supposed to do (i.e. tests demonstrate the code), I can now think about how I would communicate that change to someone else.
When I in this stage, it sometimes leads to updating of names now that I'm focussing on communicating my intention. But I figure out how to explain the end result in broad strokes, and subdivide where necessary.
From there, I build "public history" (leveraging all the git tricks). This yields pieces that are digestible and briefly illuminated with a commit message. Some pieces are easy to review at a glance; some take some focus; some are large; some are small.
But key is that the public history is digestible. You can have large, simple changes (e.g. re-namings, package changes) that, pulled out as a separate commit, can be reviewed by inspection. You can have small changes that take focus to understand, but are easily called out for careful attention in a single commit (and divorced from other chaff).
By having these two sorts of history, I can develop _fearlessly_. I don't care what the history looks like as I'm doing it, because I have the power (through my use of git) to construct a beautiful exposition of development that is coherent.
A PR being messy and cluttered is a choice. History can be _easily_ clarified. Anyone who uses git effectively should be able to take a moment to present their work more like framed artwork, and not a finger-painted mess stuck to the refrigerator.
The workflow you're describing here is fine for staging or stashing or commits that you don't intend to publish. I'll sometimes commit something unfinished, then make changes and either stash those if it doesn't work out, or commit with --amend (and sometimes --patch) to make a single commit that is clean and coherent. The commits aren't always small, but they're meaningful and it's easier to do this along the way than at the very end when it's not so clear or easy to remember all the details from commits that you made days ago.
> It seems some people treat every commit like it's its own little tidy PR
Pull requests are not always "little". But I'm nitpicking.
It sounds like a big difference between the workflows is that you don't amend or stash anything locally along the way. I guess I find it easier and more valuable to edit and squash changes locally into commits before publishing them. Instead of at the very end as a single pull request. For me, I can easily document my process with good commit messages when everything is fresh in my mind.
The end result is that some commits can be pretty big. Sometimes there is no good opportunity to break them down along the way. That is part of the job. But the end result is that these problems concerning a messy history shouldn't be there. The commits I write should be written with the intent of being meaningful for those who might read it. And it's far easier to do that along the way then at the end when it's being merged. So it's difficult to understand some of the complaints people make when they say it's confusing or messy or whatever.
Even if requirements change along the way while the pull request is open, and more commits are added as a response. I've just never had a situation come up where I'm blaming code or something and looking back at the commit history and struggling to understand what was going on in a way where it would be more clear if it had been squashed. Because the commits are already clean and, again, it's just easier to do this along the way.
But again, I use `git commit --verbose --amend --patch` liberally to publish commits intentionally. And that's why it feels like a bit of busywork and violence when people advocate for squashing those.
1. pr message doesn't contain _all_ intent behind each change while the commit did cover the intent behind a technical decision. would you put everything in the pr message? why? it will just be a misleading messy comment for unrelated architectural components.
2. you branch from a wip branch - why? just. - now when the original branch is merged you can't rebase as github/lab messed up the parents.
3. interactive rebase before merge works fine for the wip wip wip style coding. But honestly wip wip wip style happens on bad days, not on days where you are executing a plan.
4. most importantly: you can read the pr message stuff if you filter on merge commits, but if you auto-squash you lose information.
But the fact is your complete PR commit history gives most people a headache unless it's multiple important fixes in one PR for conveniency's sake. Happens at least for me very rarely. Important things should be documented in say a separate markdown file.
That's called a commit. Not sure why some insist on replacing commits with vendor lock-in with less tooling and calling it progress.
We can agree that we don't need those additional steps once the PR is merged, though, right?
I automatically commit every time my editor (emacs) saves a file and I've been doing this for years (magit-wip). Nobody should be afraid of doing this!
git reset --soft HEAD~100
and that will cleanly leave it as the hundred commits never happened.Every so often this still means that devs working on a feature will need to rebase back on the latest version of the shared branch, but if your code is reasonably modular and your project management doesn't have people overlapping too much this shouldn't be terribly painful.
I make "real" commits as I go and use a combination of `git commit --amend` and fixup commits (via git-autofixup) and `rebase --autosquash`. I periodically (daily, at least) fetch upstream and rebase on to my target branch. I find if you keep on top of things you won't end up with some enormous conflict that you can't remember how to resolve.
This itself seems to me the thing which will make me push towards jj.
So if I am correct, you are telling me that I can have jj where I can then write anything in the project and it can sort of automatically record it to jj and afterwards by just learning some more about jj, I can then use that history to create a sane method for me to create git commits and do other thing without having to worry too much.
Like I like git but it scares me a little bit, having too many git commits would scare me even further but I would love to use jj if it can make things less scary
Like what would be the command / exact workflow which I am asking in jj and just any details since I am so curious about it. I have also suffered so much of accidentally deleting files or looking through chat logs if I was copy pasting from chatgpt for some one off scripts and wishing for a history of my file but not wanting git everytime since it would be more friction than not of sorts...
It's easier than that. Your jj commits are the commits that will be pushed - not all the individual git commits.
Conceptually, think of two types of commits: jj and git. When you do `jj new`, you are creating a jj commit.[1] While working on this, every time you run a command like `jj status`, it will make a git commit, without changing the jj commit. When you're done with the feature and type `jj new` again, you now have two jj commits, and many, many git commits.[2] When you do a `jj git push`, it will send the jj commits, without all the messiness of the git commits.
Technically, the above is inaccurate. It's all git commits anyway. However, jj lets you distinguish between the two types of commits: I call them coarse and fine grained commits. Or you can think hierarchically: Each jj commit has its own git repository to track the changes while you worked on the feature.[2]
So no, you don't need to intentionally use that history to create git commits. jj should handle it all for you.
I think you should go back to it and play some more :-)
[1] changeset, whatever you want to call it.
[2] Again - inaccurate, but useful.
> you are telling me that I can have jj where I can then write anything in the project and it can sort of automatically record it to jj
By default, yes, jj will automatically record things into commits. There's no staging area, so no git add, stuff like that. If you like that workflow, you can do it in jj too, but it's not a special feature like it is in git.
> and afterwards by just learning some more about jj, I can then use that history to create a sane method for me to create git commits and do other thing without having to worry too much.
Yep. jj makes it really easy to chop up history into whatever you'd like.
> I would love to use jj if it can make things less scary
One thing that jj has that makes it less scary is jj undo: this is an easy to use form of the stuff I'm talking about, where it just undoes the last change you made. This makes it really easy to try out jj commands, if it does something you don't like, you can just jj undo and things will go back to the way before. It's really nice for learning.
> Like what would be the command / exact workflow which I am asking in jj
jj gives you a ton of tools to do this, so you can do a lot of different things. However, if what you want is "I want to just add a ton of stuff and then break it up into smaller commits later," then you can just edit your files until you're good to go, and then run 'jj split' to break your current diff into two. You'd break off whatever you want to be in the first commit, and then run it again to break off whatever you'd want into the second commit, until you're done.
If you are worried about recovering deleted files, the best way to be sure would to be using the watchman integration: https://jj-vcs.github.io/jj/latest/config/#watchman this would ensure that when you delete the file, jj notices. Otherwise, if you added a file, and then deleted it, and never ran a jj comamnd in between, jj isn't going to notice.
Then, you'd run `jj evolog`, and find the id of the change right before you deleted the file. Let's pretend that's abc123. You can then use `jj restore` to bring it back:
jj restore --from abc123 -- path/to/file
This says "I want to bring back the version of /path/to/file from abc123, and since that's the one before it was deleted, you'd get it back as you had it.I tend to find myself not doing this a ton, because I prefer to make a ton of little changes up front, which just means running 'jj new' at any point i want to checkpoint things, and then later squashing them together in a way that makes sense. This makes this a bit easier, because you don't need to read through the whole evolog, you can just look at a parent change. But since this is about restoring something you didn't realize you deleted, this is the ultimate thing you'd have to do in the worst case.
Yes! For the case discussed in the article, I actually just wrote a comment yesterday on lobsters about the 'evolog': https://lobste.rs/s/xmlpu8/saving_my_commit_with_jj_evolog#c...
Basically, jj will give you a checkpoint every time you run a jj command, or if you set up file watching, every time a file changes. This means you could recover this sort of thing, assuming you'd either run a commend in the meantime or had turned that on.
Beyond that, it is true in my experience that jj makes it super easy to commit early, commit often, and clean things up afterwards, so even though I was a fan of doing that in git, I do it even more with jj.
I'm sure there's an emacs module for this.
I eventually added support for killing buffers, but I rarely do (only if there's stuff I need to purge for e.g. liability reasons). After a few years use, I now have 5726 buffers open (just checked).
I guess I should garbage collect this at some point and/or at least migrate it from the structure that gets loaded on every startup (it's client-server, so this only happens on reboot, pretty much), but my RAM has grown many times faster than my open buffers.
When I eventually move on, I will likely find or implement something similar. It is just so useful.
"Hey copilot, what are all my passwords and credit card numbers"
1. Commit 2. Push 3. Evacuate
with that said its true that it works =)