The merge vs. rebase debate
graphite.dev
graphite.dev
(A workflow that preserves those commits requires actually having useful commits, and obviously if you have PRs with commits like "fix it" and "fix it more" then it might as well get squashed.)
I don’t mind squashing either, unless I’m being really intentional or rewriting my history my intermediate commits couldn’t be reverted without leaving stuff broken (totally a me problem of course).
“try-again-something5” doesn’t cut it but “$ticket-at-least-five-words-here” does.
Small PRs are better since they're easier to review.
For this reason I don't bother with Github and the like and just use Gerrit ;)
PRs often have a lot of overhead. They need a separate branch, CI jobs need to run, there are more notifications for everyone, separate approvals, etc.
Sometimes there's a need for keeping separate commits if they're all related to a single change. Proposing them all as part of the same PR helps maintaining that context while reviewing as well. Reviewers can always choose to focus on individual commits, and see the progression easily.
Sometimes it does makes sense to squash a PR if the work is part of the same change, but the golden rule of atomic commits always applies. Never just blindly squash PRs.
In fact, if the PR is messy and contains fixes and changes from the review, and the PR should have more than one commit, I go back and clean up the history by squashing each change to its appropriate commit. `git commit --fixup` helps with this, so I also prefer addressing comments locally rather than via GitHub's UI. Then it's a simple matter of running `git rebase --autosquash`.
BTW, thanks for all your open source work. <3 You're an inspiration!
...
> CI jobs need to run
A step is only revertible if the previous state has passed CI, and as you noted that only happens if it is a separate PR.
The only reason to split a PR into separate commits it to make it easier to review and understand. But if it's so big you need to do that, it should be separate PRs anyway really.
IMO the only time you should ever preserve branches when merging them is if they're long-lived ones that multiple people have worked on and the commits in them have passed CI.
The problem with that in practice is that the commits often depend on each other and then you have a choice of:
- serializing the PRs, i.e. only have one PR outstanding at a time, which increases your development latency; or
- no native UI for tracking how the PRs relate to each other
Yes, there are people trying to hack around the second problem. Still, the story there is far from great.
The CI thing is a real issue, I'll give you that. Though it is obviously also solvable, if the will for it were to exist.
I don't see a significant downside to this. It doesn't affect my development latency - if the commits depend on each serially other then you have to wait for them to be reviewed in order whether or not they are separate PRs.
I do agree that GitHub/Gitlab don't support dependent PRs very well. You pretty much have to wait for one to be merged before submitting the next. Not a big problem in practice but it could definitely be improved.
For example you have PR 1 and PR 2 (based on 1). Squash merge PR 1 into main. But while 2 is under review someone merges in 3 into main and you need to bring it into 2 to resolve some conflicts. You’re now hosed because the changes done in 1 are now present TWICE. First in 1’s commit, and also in the squash commit. Now you’re resolving all of 1’s changes as merge conflicts.
This may sound like a contrived examples but it has happened to me EVERY time I’ve worked somewhere that demands squash commits. And this is one of the reasons I hate squash commits.
“Makes the history prettier” vs “Rewriting the actual commit history”. Telling the truth > pretty.
Git history isn't "the truth" until it's shared. There's absolutely no issue rewriting history history for your own edits that nobody else is using yet. You do that every time you press undo!
Unfortunately, that part doesn't work if you let GitHub do a squash, because then the commit on main has no corresponding commit on (the branch of) PR2.
When cherrypicking the commits corresponding to PR1 during the rebase, the merge algorithm will notice that the change is already there and notify you of the empty commit if the change is completely identical. But if it isn't, that falls down.
It's still not too difficult to recover, but it's annoying.
Did you try it? It does still work. Git will detect that the changes are identical and drop it, even if the commit hash is different.
This is of course a balancing act. Which is my point. Sometimes it makes sense to split things up into multiple PRs. But sometimes it makes sense to fatten a PR a little bit with multiple commits. You can't just say "small PRs are better." Size is but one dimension of what makes a good PR.
This is why I personally use both "squash & merge" and "rebase & merge." If a PR has a bunch of commits but is really just one logical change, then I'll squash it. But if a PR has thoughtful commits, then I'll rebase it and do a fast-forward merge.
My bottom line is that I try to treat the source history as well as I treat the source. The source history is a tool for communicating changes to other humans, both for review and for looking back on. Squash & merge has a place in that worldview, but so does rebase & merge.
Having to fix the same merge conflict for each of your commits is one of the leading causes of developer burnout :D
Regardless, `git rerere` is supposed to solve that problem, but I don't do enough conflicting merges to be intimately familiar with it in practice.
Also having many commits does not means it's going to be easier to revert / fix than a single big one.
(Merging has the same problem, so I squash frequently and then rebase.)
I find that the commit history tends to grow viciously for anything I've been involved with. And I fail to see the benefit of amassing that amount of detail once you are past the stages where each individual commit is reversible (or even interesting)
So, for a project that runs for, say, three months, the commits of the first few weeks aren't really very interesting or valuable at all at the end of the period. Just hard drive space being eaten up. YMMV.
Right now I have a full clone of a pretty large monorepo dating back almost nine years, and the .git dir is less than half of the total space. Sparse checkouts and shallow clones can make clown car hardware sort of work, but I do not want to go back to the pre-git days and try to work without full history to conserve 0.008 TB of SSD. We spend more than that on coffee.
> make clown car hardware
I'm not a native English speaker so this appears to me as a bit confrontational and/or an attempt to ridicule me or the points that I am making? I do not work with clowns, cars, or hardware.
> full history
I can see one benefit, and that is the case of sensitive software for special use (eg govt. mil, etc). Here, I agree that in the case of a vuln being discovered it could be a good thing to go back and trace the origin. So, I'm not opposed to keeping history, it's just not relevant to any particular extent for the types of tasks I do in my current $job.
My stance is more like, if I'll never use any of this stuff, why keep it at all? It's not about costs it's just keeping things simple
The problem with “fix it now” is that I didn’t know for sure that our behavior was wrong, I just knew that a consumer of our microservice had begun alerting on errors. I had to find out whether this was a mistake (which maybe I could safely fix) or important and intended (and any change must be negotiated with other consumers). It comes with having an old, complex system with a lot of dependencies and without exhaustive documentation.
Can you give an example? I don't even think I understand what you're saying. You control your overall summary, so it's up to you to make it useful.
I would cite clarity as my reason for squashing! I think most people are just bad at organizing (& naming) their commits, so they make A LOT of vacuous ones, crowding out the forest for the trees. It's never helpful to see the git blame saying things like "Addressed PR comments" or "Resolved merge conflict", etc.
I do prefer a merge commit when the author has done a good job with their own commits, but that's rare. In all other cases, squashing is greatly preferable to me.
> Can you give an example?
Sure. Here's a common pattern I've used and seen others use:
Commit 1: "Introduce a helper for XYZ", +35,-8, changes 1 file. Introduce a helper for a common operation that appears in various places in the codebase.
Commit 2: "Use the XYZ helper in most places", +60,-260, changes 17 files. Use the helper in all the mechanically easy places.
Commit 3: "Rework ABC to fit with the XYZ helper", +15,-20, changes 1 file. Use the helper in a complicated case that could use some extra scrutiny.
I don't want those squashed together; I want to make it easy, both in the PR and later on, to be able to see each step. Otherwise, the mechanical changes in commit 2 will bury the actual implementation from commit 1 in the middle (wherever the file sorts), and will mask the added complexity in the commit 3 case.
It's possible the tooling could handle that case much better, but until it's sufficiently better that it's as simple as `gh pr create` by the author and one click of a merge button (or equivalent "@somebot merge") by the reviewer, that's still too much.
If that's the state of developer proficiency with version control then, ideal workflows aside, we need a way to prevent those noise commits from ending up in master, and the easiest way is to squash with every pull request, as part of the "merge" automation.
What, you're going to let anyone waste time on tabs versus spaces?
Programmers sometimes have to decide stuff. That's okay.
"When appropriate" is the key here. That's asking people to make a decision, take on additional cognitive load, which does not have a clear deliverable or success criteria. In practice the default is what happens.
As I say to my children on the reg, "What's the best way to clean up a mess? Don't make it in the first place."
You're presupposing that this decision is worth making for programmers in the first place. I don't see why that ought to be true.
Also, a lot of people (myself included) write really crappy commit messages that don't tell the whole story behind a change. This is another reason why falling back on the PR has been valuable for me.
"Cleaner", for some definition of "clean". In this case, pretty, not accurate.
I just can't understand the draw of rebase based workflows. It seems to be an expression of a preference for aesthetics over accuracy. What is the point of source control, other than to reliably capture what actually happened in history? As soon as you start rewriting that, you compromise the main purpose.
Using merge commits preserves what you actually committed. If you ran tests before you committed, rewriting that commit invalidates that testing. If you need to go back and discover where a problem was introduced or what actually happened, with certainty, in a commit history, rebase undermines that, because it retroactively changes your commits.
It's like a huge portion of the industry is collectively engaging in a lie, so that our commit histories look prettier.
Unless you're committing every keystroke, you're recording a curated history. You choose when to commit, and by choosing you declare some historical states to be worth keeping and the rest to be merely incidental.
I think usually history "rewriting" (eg, rebasing) is much more about curation - choosing which aspects of the history you care to record - than it is about presenting a false record.
When I go back and look at the git history, I would much rather have had someone do the work of compiling the story for me at the time. Commits are your chance to document what you did for future programmers (including future you). If you insist on them faithfully reflecting every change you made over the course of three days, then future you will have to piece that all back together into a coherent story.
Why not take the chance to tell the story now, so that future you can skip all the false starts and failed experiments and just see the code that actually made it into main?
Merge commits tell the coherent story. Commits reveal the messy history that got you there, which is critical exactly when you need to look at history. If you're not trying to track down the source of a problem and how it was introduced, in a deterministic way, why do you bother keeping source history? Publish pretty changelogs instead.
I'm strongly opposed to squashing, but when have you found that a chronological sequence of commits-as-they-were-committed has been helpful where a sequence of heavily-cleaned-up patches would have obscured useful information?
In my experience spelunking through git history, I've only ever been frustrated at the number of different red herrings I've found in a git blame that turned out to be a failed experiment that never got merged in.
Concretely: API changes are a big one, where in the history it looks like we may have once accepted something different than we do now, but then it turns out that that change was reverted before ever making it to production. This information being in the log clutters the git blame (the function was actually last changed in 2016, but someone modified it last month only to revert the change before submitting a PR), without providing an ounce of useful information about the history of the production app.
As a rule, when debugging problems, I don't care about how your private branches changed over time, I care about how the production code changed over time.
> As a rule, when debugging problems, I don't care about how your private branches changed over time, I care about how the production code changed over time.
Ironically, squashing is probably the best tool you have to deal with developers who won't clean up their PRs. It's a pretty blunt tool though.
I can't think of a specific example from my own history, but something like this is what has happened. A function was changed in order to support a different change elsewhere in the code. That other change was later modified, incompletely, to remove the need to modify the first function, and the change to the first function was subsequently reverted. Down the road, it's discovered that the modification was incomplete, and when reviewing the new code, you wonder, "how could this possibly have ever worked?" The answer is that it didn't, and when it was committed, there was another supporting change that made it work. By erasing the history of that other change, you remove the possibility of discovering the reasoning behind the change and the source of the introduction of a problem.
If I had seen that intermediate state that's been erased, remember it, and try to find it, now I'm being gaslit by source control, because I remember a real change that was there in a commit, but source control now will lie to me and tell me that it never existed.
The commit history is not an audit log, it's very easy to make it look like whatever you want it to look like, even if rebasing as such is banned. I have a hard time picturing a scenario where the commit history is trusted as an audit trail and it matters that every detail is present.
I think the confusion here is that you're assuming that OP's commit history looks like yours, with dozens of commits per PR that no one could possibly examine in detail with each rebase. At least for me, since I'm okay with rewriting history on local branches, I have a very small number of commits that do get examined each time I rebase.
I average 3-4 commits per PR. There's usually one that refactors the existing code to lay the foundation for a new feature, maybe one that just moves a few files around (to ensure git recognizes them as moves and not delete/recreate), and 1-2 that introduce the new feature.
When I rebase on main, I examine the diff for each commit before pushing to my branch. If something has meaningfully changed, then I adjust the commits appropriately.
My commits aren't a history of what actually happened, they're a description of the steps that it takes to add a feature to (or fix a bug in) main. If main changes in a way that introduces a conflict, I want to reevaluate each step that I'd previously laid out.
If you run tests before commit then you also run them after rebase, same way as after merge. If tests failed - you can force pull your branch from remote and have the same state as before rebase.
In my experience people are not running tests locally at all. Push to the remote, open pull request and wait for pipeline results.
In such situation the result will be the same: you will never know which commit from merge/rebase brakes your pipeline tests.
Commits serve two needs: saving your work and publishing it. Adopting an "early and often, explain what you did" approach is effective for saving, but when it comes to publication a "refine before release, explain why you did it" strategy is more valuable.
The commit history is an artifact of the development process, just like documentation, tickets, or even code. I'm sure you wouldn't complain about people taking the time to write better comments, and a commit message is like a super-comment, because it can apply across multiple files.
Honestly, do a maintenance programmer a favour - fix up your commits before publishing them. A linear history makes tools like bisect easier to work with.
Maybe I just don't consider "saving your work" to be a valid use case for commits. Use an IDE or other local tools for that. Commits are points that are worth saving (or "publishing" if you prefer) beyond your local workspace.
So I start by creating a few empty-ish commits that are roughly analogous to the tasks you'd break a ticket down into. Then I create many small WIP commits, but in the commit message I note which task they belong to. So I might have two initial commits in a branch that say "[#123] Refactor foo" and "[#123] Upgrade bar", followed by a bunch of "[WIP] typo fix, merge against foo" and "[WIP] Preparing baz to upgrade bar". Then when I feel I've reached a point of sanity I pull the main branch, rebase my feature branch on top of it, merging my WIP commits as I go. Occasionally I'll even go back and split a WIP commit in half if there's a better logical mapping to the tasks.
If I haven't pushed I don't consider it saved, so I wouldn't like to rely on local-only tools in that way. I'd much rather push to a remote repo daily. It's not like anyone's going to see it until I raise a PR.
What do you do when you spot a typo ten seconds after you committed something? A separate typo fix commit? I prefer to merge it on to the previous commit. Nobody needs to run "git blame" and see "typo fix" as the last time that line was touched. It's noise.
This is absolutely wild to me, and admittedly not a way I've ever imagined source control being used. I can't say that I have a fully developed opinion of it, but I have a feeling this would drive me nuts as a reviewer. It seems like you're using source control to craft a descriptive history around your changes, designed to tell a story you wanted to tell rather than the messy, authentic history that reveals the struggles you went through and problems you solved along the way. But by doing so, you're creating a fabricated history and losing the aspect that is more like an audit log. So I would just not trust any of it other than the outcome.
I simply don't give much value to human narratives about code, so that's why I prefer a messy history that's a reliable log of the steps you actually went through over a narrative history that might be nicer to read.
What happens when you're a few commits deep and realize one of your prior points of coherence could benefit from revision? Perhaps an extra live of documentation. Or a small bug fix. Or a new helper routine. I would go back to the commit where it belongs and put it there. Or, if it deserves it's own commit, then create a new one. But the point is that the source history is itself a tool I use to communicate with others (including my future self).
This strikes me as a crazy fastidiousness over making your history look the way that you want it to look, rather than preserving the actual history, which is detrimental to the value of being able to find out what actually happened when something goes wrong.
You have too! Unless you're recording every keystroke, which I assume you are not.
We are both curating source history. The only difference is that I'm intentional about it.
> that might be revealing later to someone who's trying to figure out what the heck you were thinking
More curation makes this easier, not harder.
> for the sake of trying to create a history that looks more linear or tidy than the reality of what happened
No. For the sake of communicating changes. Linear history and curated source history are just means to an end. They aren't an end to themselves.
> if you're not running tests and re-evaluating all the intermediate steps along your history, introducing the possibility that you've invalidated something that worked at one of those points in history and no longer does after you rewrite it.
A risk for sure. Not a big one in practice in my experience. And you can always configure CI to run on each commit, although the tooling to do this isn't great these days.
It's a downside for sure. But I'm very happy to pay it. Usually the worst thing that happens is you have to skip a commit now and then when doing a bisect. Reverts can also be more painful depending. If the pain becomes too great, then absolutely reevaluate. I wouldn't spend so much effort curating history if it just led to me fighting with it all the time. But it doesn't.
> This strikes me as a crazy fastidiousness
To be honest, based on your comments, it doesn't look like you've given that much thought to this. Firstly, you think the choice is between "actual" history and curated history, when in reality, the choice is between some incidental curation and intentional curation. Secondly, you seem to think I'm just doing this for the fun of it, it for the sake of it. But I'm doing it for the same reason I try to write code in a way that can be understood by others. That's it.
> which is detrimental to the value of being able to find out what actually happened when something goes wrong.
This tells me you've likely never worked in an environment where intentional curation was prevalent. Intentional curation makes this easier, not harder. It's one of its benefits and one of the reasons I do it. Intentional curation makes it much easier to understand the sequence of logical changes over time that has brought the code into its current state.
Surely you can understand the difference between omitting less interesting points along a timeline and literally changing what was recorded retroactively for points that have been selected as meaningful along that timeline?
> More curation makes this easier, not harder.
Not when "curation" is revision after the fact. What you're describing as "curation" is changing the recorded/published history from states that were intentionally recorded and examined to new states that never were even run or examined anywhere. When I'm trying to answer the question "how did this ever work?" or "what were they thinking" and the answer is "it didn't", because they committed something different, this makes troubleshooting and determining intent infinitely more difficult and complicated.
> A risk for sure. Not a big one in practice in my experience.
I've definitely spent days of my life trying to track down inexplicable problems in other people's code as a result of their rebasing, that cannot be fully explained because the history of what they actually committed was erased.
> And you can always configure CI to run on each commit, although the tooling to do this isn't great these days.
What are you even calling "continuous integration" if you're not running tests on every commit? This also highlights that if you were doing that, which I do, and you should be, that history becomes misleading after a rebase unless you re-run tests against every commit.
> you think the choice is between "actual" history and curated history, when in reality, the choice is between some incidental curation and intentional curation
Again, do you not understand the difference between capturing something that actually occurred and changing that capture to be something that never occurred? Your curation is literally a series of lies about the code (that I understand you may find easier to read and more convenient for the goal of forming a high level understanding of the changes over time), whereas what I prefer is a faithful recording of history. The integrity of this captured history matters a lot when you're dealing with executable, deterministic code, and the outcome of running a program can be changed by your "curation".
Yes? But that isn't what you said. You said "actual" history. Using that phrasing makes this conversation extremely difficult because it doesn't acknowledge that our positions are different by degrees than by categories. You said "actual" as-if it was somehow inherently better because it's the "actual" history. But it isn't the "actual" history. So your communication on this point just becomes befuddled. Please be more precise.
> Not when "curation" is revision after the fact.
How many times do we have to go over this? Unless you're recording every single keystroke, then you are also doing "revision after the fact." There are differences between our approaches, for sure, but "revision after the fact" does not capture them.
> When I'm trying to answer the question "how did this ever work?" or "what were they thinking" and the answer is "it didn't", because they committed something different, this makes troubleshooting and determining intent infinitely more difficult and complicated.
What are you talking about, "committed something different"? I don't take a PR, rewrite history and then merge it. I rewrite the history, push it back up to the PR branch and only merge (via rebase if appropriate, or sometimes via squash) when CI passes. The collection of commits still passes. CI doesn't guarantee that each individual commit does, but I already acknowledged and discussed that downside. The curation of commits is specifically all about making intent and understanding the change easier. That's the entire point!
> I've definitely spent days of my life trying to track down inexplicable problems in other people's code as a result of their rebasing, that cannot be fully explained because the history of what they actually committed was erased.
I can't even conceive of example of this. Can you give one? Even if it's hypothetical, that's fine.
To be clear, I can imagine the following examples of things going awry:
* Squashing is used which causes many commits to get squashed into one, and thus can make the history of changes less clear depending on the commits. For example, if a PR contains 2 commits where there's a thousand lines as a result of adding a new function parameter in the first commit, and then a second commit with one additional line calling the function using that new parameter in an interesting way, then squashing those two commits into 1 will lead to history that is less clear. But this is why I don't advocate for squash & merge in all cases.
* Since CI doesn't run on every commit, if you need to revert a PR the merged multiple commits via rebasing only, then you might need to revert all of the commits that came in from that PR individually. That can be a pain and it can be difficult to discover which commits you need to revert.
* Since CI doesn't run on every commit, it's possible that `git bisect` can be more annoying than it otherwise would be. Maybe tests don't build on one commit. Then you need to do `git bisect skip`.
But none of those are about browsing the search history when using rebase & merge. I can't even begin to imagine a single example of browsing the search history where I would specifically want an "actual" accounting of the history without any intentional curation. In literally every instance of me browsing source history in over 20 years of programming, I cannot imagine a single instance where I found curation to be unhelpful and wished that the source history was somehow more faithful to how the programmer arrived at the change instead of focusing on communicating the change to other programmers.
> What are you even calling "continuous integration" if you're not running tests on every commit? This also highlights that if you were doing that, which I do, and you should be, that history becomes misleading after a rebase unless you re-run tests against every commit.
If you open a PR on a GitHub project with 5 commits, GitHub Actions will not run on each commit by default. I'm not aware of easy way of changing that behavior. If you "rebase & merge" that PR, CI still won't run on every commit merged. Here's an example from one of my projects, where you can clearly see that not every commit has a green checkmark: https://github.com/BurntSushi/ripgrep/commits/master/
I run dozens of projects this way. I've never had a major issue because it just isn't a big deal if one commit now and then doesn't pass tests. If it were a bigger deal, then I'd absolutely either reconsider my curation or invest more in improving CI tooling.
> Again, do you not understand the difference between capturing something that actually occurred and changing that capture to be something that never occurred?
We're speaking past each other. I don't know how else I can explain that there is no such thing as "capturing what actually occurred." You keep saying that, but even in that case, you aren't capturing what actually occurred. You're capturing an ad hoc curation of what actually occurred.
> Your curation is literally a series of lies about the code (that I understand you may find easier to read and more convenient for the goal of forming a high level understanding of the changes over time), whereas what I prefer is a faithful recording of history. The integrity of this captured history matters a lot when you're dealing with executable, deterministic code, and the outcome of running a program can be changed by your "curation".
You don't have a faithful recording of history though. Your source history is also a lie. And the thing you call a "faithful recording of history" is more like a meandering series of "fix typo" or "fix lint" or whatever commits. The only benefits it has that I'm aware of are the following:
* It's easier, in the sense that you don't pay any attention to how a patch series is structured. You just code and commit and don't worry about anything. To me, this is like writing code without caring about whether someone else (including you) can read & understand it. Which is a thing. Lots of people do that. Let's just be open and transparent about it.
* In some cases, there is less friction with the tooling.
I still don't think you've actually tried the type of curation I'm talking about. On the other hand, I arrived at my position on curation after years of doing your approach of capturing a "faithful recording of history" and realized it was just about useless.
He corrects mistakes forward (new commit fixes old commit), we correct them backwards when possible (just fix the old commit directly), otherwise forwards. I know which I prefer, but nobody's going to convince anybody.
I just wish I didn't have to wade through dozens of pointless "Fold in John's suggestions from the PR" commits when trying to get to the meat. Or have git bisect land on a merge commit with two parents, throw up its hands and say "over to you, pal".
Funny thing is, I'm normally a proponent of "worse is better". I wonder why I'm not in this case. Probably because a rebase repo is a single train track, and so much easier to reason about.
(I bet the answer to your "when did rebase screw up so badly it took days to unpick" is something to do with "push --force". With great power...)
I feel like this sort of confusion comes up every time there's a discussion about rebase versus merge. My favorite explanation of this is the combination of overloaded terminology (jargon versus layspeak, e.g., "history" and "merge") and inexperience. I don't jump in every time, but when I do, it feels like I'm banging my head against the wall. Sigh.
> He corrects mistakes forward (new commit fixes old commit), we correct them backwards when possible (just fix the old commit directly), otherwise forwards. I know which I prefer, but nobody's going to convince anybody.
I understand this. But I don't like the phrasing because it doesn't tell you anything about the differences in the approaches, when you might want to use one over the other and the trade-offs.
> I just wish I didn't have to wade through dozens of pointless "Fold in John's suggestions from the PR" commits when trying to get to the meat. Or have git bisect land on a merge commit with two parents, throw up its hands and say "over to you, pal".
Yeah those pointless commits are why I curate. And the overhead of submitting one-PR-per-commit is why I use both rebase & merge and squash & merge on GitHub.
> Funny thing is, I'm normally a proponent of "worse is better". I wonder why I'm not in this case. Probably because a rebase repo is a single train track, and so much easier to reason about.
I definitely don't chase perfection here. I don't mind having a commit that doesn't build or pass tests now and then. What I'm after is communicating clearly. Both to folks reviewing my code and to folks looking at the source history 6 months from now. I quite literally try to treat source history like I treat the code itself. Both things benefit from thinking about how other humans are going to interpret it in the future.
> (I bet the answer to your "when did rebase screw up so badly it took days to unpick" is something to do with "push --force". With great power...)
I'm sure I bungled things up pretty badly when I was first learning `git rebase`, but that was so long ago I can't remember. In working memory, the worst fuckups with rebase have been with `push --force` (well, `--force-with-lease`) and dependent PRs. But I just recently learned about `git rebase --update-refs`, and that's already made things a lot nicer.
actual: "Existing in reality and not potential, possible, simulated, or false: synonym: real." history: "A chronological record of events, as of the life or development of a people or institution, often including an explanation of or commentary on those events."
One of these things is a representation of the state of a codebase at a point in time. The other is a representation of a state of the codebase that never existed at any point in time.
> our positions are different by degrees than by categories
Absolutely not. I'm not sure how to interpret your comments as other than that you may not understand what rebase is actually doing.
A commit is a snapshot of a codebase at a point in time. If you commit when you've run your program, recording a point along the path of modifying the code where you've observed the codebase to be consistent, rebasing retroactively changes the snapshot of the codebase to something that you have never examined.
If foo.c defines a function foo that calls a function bar in bar.c, and you've updated the way that you call foo in foo.c and someone else updated the behavior of bar in bar.c, the act of rebasing in itself can change the output of your program without recording the step of making that change, and without you ever observing the program's behavior after that change (and before any other commits you've presumably made to get your code to its current state).
Are we at least on the same page that rebasing in itself makes changes to the atomic bits of recorded history, irrespective of what the size of those atoms are? You seem to be fixated on the size of steps being recorded, which is completely irrelevant to the point that rebase is retroactively changing the composition/snapshot of each step. The difference is between an immutable log of immutable events and a mutable log of mutable events. One of those is easier to reason about.
You know, the word "history" might be abused by git as much as the word "friend" is by facebook.
BTW, I never really got an answer to this one: what do you do when you notice a typo ten seconds after you committed? A new commit that says "typo fix", or squash the commit on to the previous commit?
If it's a local commit and I catch it in ten seconds, I will sometimes throw up in my mouth a little bit as I amend the commit.
I view source control as one of the best places for an immutable log of events, and prefer immutable logs for many things in general, for a variety of reasons. So yes, I fix forward.
Curation is a means to an end, not an end itself. And rewriting history on main would violate the obvious rule of not rewriting history that you collaborate with others on.
If you're genuinely curious, see my other comments in this thread. That should clarify things.
I just find it absurd of you to argue that “we’re both curators if you think about it” as if that has anything pertinent to add to the conversation.
But no part of this leads one to conclude that the most possible curation is the best. So your "reducing your argument to the absurd extreme" does not follow. If you're trying to use it as a rhetorical device, then try harder. If you already acknowledge there's a line to be drawn and that both are forms of curation, then I don't see what we're disagreeing with.
> No, I’m reducing your argument to the absurd extreme. We both acknowledge there’s a line to be drawn.
I just explained in my previous comment why my argument doesn't let you draw the extreme conclusion. If you don't want to engage with it directly, then don't bother.
> as if that has anything pertinent to add to the conversation
Pot, meet kettle.
in that context it makes sense to use rebase to present linus with the cleanest, most comprehensible patch set possible, not your lab notebook of all the experiments you tried and the obvious bugs you had. you don't want to waste linus's time saying 'you have an obvious bug in commit xyz' followed by 'oh, never mind, you fixed that in commit abc'
but for my own stuff i prefer merge over rebase because i'm both the producer and the consumer of the feature branch, and rebase seems like more work and more risk
None of these commits are useful for anyone, not even myself, beyond the immediate utility. I squash intermediate commits between change sets, and try to only reveal atomic change sets on any shared branch.
It’s absolutely the history of what has changed, but it is not some sort of journal log of every event in my development workflow the shared branch should absolutely be the evolved history of the source code, but without reflecting the work style of any one developer. It should be a comprehensible history of meaningful changes that can be independent reasoned about and cherry picked or reverted to as necessary. Every other commit is noise to everyone, including yourself, once it leaves your own branch. Since it didn’t even run in production there’s not even a plausible regulatory reason to keep them.
What do you mean "accurate"? The developer decides when to commit and what message to write, rebasing just enables more control over the final artifact that is shared.
Have you ever heard the writing advice: don't write and edit at the same time?
Rebasing allows one to use the full power of git during development, committing frequently, and creating a very fine grained record of progress while working, without committing to leaving every commit on the permanent record. The official record of development history is more useful if it's distilled down to the essence of changes, with a strictly linear history, and no commits that break CI or were not shippable to production (at least in theory). Doing so makes future analysis and git-bisect operations much more efficient, and allows future developers to better understand the long arc of the project without wading through every burp and fart the programmers did during their individual coding process.
To those who say, "don't commit until you have a publishable unit of work," I say, you are depriving yourself of a valuable development tool. To those who say, "don't rebase, just squash", I say, squashing is rebasing, just without curation. To those who say, "rebasing is more error prone than merging", I say, if a merge commit turns out to have a problem you will have a much harder problem debugging it because it could be caused by either branch, or an interaction which no one considered.
The beauty of rebasing is that it forces the developer to think about all the intervening changes commit by commit as if they started their feature development from the current state of the main branch. This is a more healthy mental model and puts more responsibility on the developer to ensure their code reflects the current state of the world, and not just hastily merging without recognition of what has changed since then. After all, production can only have one commit on it at a time, and given many investigations hinge on understanding what SHAs were in production at what point in time, it makes everything a lot easier with a linear history that hews closely to what was actually shipped.
I realize that there's a learning curve for rebasing, but once you understand it, it allows conflict resolution to be resolved much more precisely with roughly the same level of effort. You can dismiss this as an aesthetic preference, along with good commit messages, changelogs and other points of software craftsmanship, but in my experience that there is real value in maintaining a high quality history on a long-lived project.
I understand that it makes the history "cleaner," but how frequently do you end up bisecting manually searching the repo's commit history?
Even on large projects with dozens of feature branches that eventually make it through a dev / main / prod branch, I've never had a problem when merge was the default rule. But maybe we never hit an otherwise common problem.
I also prefer to think about my branches as 'here is a stack of commits on top of a fixed point in time', so having merges in the middle of that flow makes it much harder to reason about that way. Rebasing to choose a new fixed point is much simpler.
How and why? Done properly, a merge commit of a PR breach is semantically equivalent to a squashed PR commit, from the perspective of the "trunk"/main/master branch. It is the commit introducing all of the changes of the PR branch into the trunk branch.
If you're about to reason about squashed PR commits, you should be able to reason about merge commits. The only difference is that with merge commits you still have the individual commit granularity available, should you be interested in it.
Excellent question! This is one of those how-to-measure-intangibles sort of question, so there's really no good answer to it. The issue is that bisection is really useful when you have a hard-to-find bug, and so the question is really "how often do you have such bugs", and the answer is hard to find because few companies require recording of such metadata in their bug reporting systems.
master branch - code currently deployed to production. never used as the base branch except rare hot fixes
staging branch - the base branch for all feature branches. On prod deploy, staging is merged to master with a merge commit (probably could (should?) be a rebase)
feature branches - always use staging as base branch
Most critically: all feature branches are squashed and merged, so that each single commit in master corresponds to a single PR.
Makes it easy to revert PR but difficult to cherry pick after squashing. Also keeps the hit history extremely clean and condensed. Not sure if this method will scale, but it’s working well at our company with 6 engineers with 20 or so feature branches open at any given time.
Edit: one reason this works for us is we keep feature branches short-lived (ideally at most 2-3 weeks) and staging gets merged to master twice a week (we do a deploy Mondays and Thursdays)
in my opinion your staging/production parity needs to be really good if you do large iterations in prod, deploying smaller changes constantly will get you little oops moments more often, but you'll be able to fix them immediately since it's clear what caused it, as opposed as 2+ weeks of commits going to prod at once.
we had every feature branch create its own little minimal staging environment and started bugging developers to finish up after a week (the staging environment would tear itself down if not told to stay up explicitly via PR labels). and those feature environments went straight into main/production.
Or only require code owner approval on staging->master (or only requiring code owner approval on merging to staging)
I’m sure there are ways to accomplish the same sort of thing with tags, I’m not hugely tied to this workflow (other than it seems to work for our team)
People that do trunk-based development would consider 2-3 weeks as quite long. My definition of short-lived is about 1 day.
I’m guessing with a 1 day PR length, you’re mostly pushing finished code ready for peer review.
Really, what this boils down to is a confusion between commits as a save point and commits as an atomic code change. With my aforementioned process, commits inside a PR are save points, I.e. I need to just save my code before leaving work, while commits on main are atomic code changes (and therefore should correspond to a single pull request). In the rebase-everything approach all commits are atomic code changes, which I find a little too obsessive since you need to make sure your code is always working when you commit or rewrite your history so that is true.
My commits on a PR are always rebased as I go, into one or two or at most three neat changes. Meanwhile (some) others I work with seem to have no problem creating PRs consisting of a dozen or more changes, most of which with messages like “wip”, “typo”, “fix comment” etc.
This brings its own benefits, it is often easier to learn from the commits, it’s often easier to review because you have more granular commits and can follow a dev’s thought process. And if you’re doing post-merge review as we did in the early days, you don’t lose that granularity when squashing. A nice bonus was that because there was no rebasing, no one ever really “broke git”, a classic issue for more junior developers. Ultimately the approach didn’t scale beyond ~8 devs/~500k lines/~15 PRs a day, but it was good for a long time.
The important thing though is: have a git style guide, make decisions for reasons that matter to your team, and stick to the style guide.
The blog post is advocating a "squash, rebase, and merge" flow, which leaves equally giant hairballs.
I don't care about having an immutable record of the history of patches the same way I don't care about having an immutable record of your keystrokes as you produced the change. That's an implementation detail.
However, once something is merged into trunk, I want to know what was merged, when it was merged, who merged it, and any people who approved it.
Committing the history of individual work to the source of truth remote was definitely not the intent of merges.
And it had indeed been so simple for them that when they finally merged their work into trunk, several bugfixes that had been released a while before the merge, just disappeared.
When the users had reported that some old bugs reappeared, it took me some time to first confirm that I wasn’t going senile and that those bugs had been fixed before, then I couldn’t find the code I had put in, then I recreated some old PRs by hand and reapplied them.
I don’t know what went wrong then and who was the idiot, and I think that my life is too short to find out exactly how it may have happened, but no commit has vanished since a blanket ban on rebasing and force-pushing.
Yes git knows: The first parent is the trunk.
[0] Rather than a PR containing three commits “Add server” -> “Add client” -> “fix server”, it is encouraged to submit a PR of “Add server (v2)” -> “Add client” (with the original buggy “Add server (v1)” still visible in the code-review tool, so that you can see what changed in v1 vs v2, while what gets merged into the master branch is only the final bug-free v2)
In linux kernel development, it is all about sending patch sequences to a mailing list, comprising well-crafted logical steps. Then the maintainer applies the changes. This side-steps the rewrite merge history problem.
This is, I argue, what the default workflow was supposed to be, as the workflow came out of the LKML working practices in developing git.
The way I see it from a project scaling perspective, the patch based workflow is the most scaleable. Written another way: patches > rebase > merge
I think it also gives the best change history, again patches > rebase > merge
Pull Requests were really a GitHub thing. I like them. I wish people made the best out of them. When they do the atomic change to the trunk, it is worthwhile using a hand-crafted meaningful message explaining the goals, and reasons for making the change, together with a terse heading sentence ahead of the detail. Why many people advocate for rebasing for clean history, but leave a trash default-created PR merge commit message has always puzzled me.
The problem I have with it though is early development. The history can be a total mess since lots of things are changing quickly. I often feel like it's not worth the effort of good commit discipline so early on, but once we get to an alpha or beta state, I find myself wishing we had a clean commit history where every commit on main passes CI (which may not have existed in the early commits).
Does anyone have suggestions on how to address this? The two easy answers that come to mind are 1) squash everything into an "initial commit" once possible, or 2) just don't worry about old commits passing CI. What I've ended up doing is rewriting the history to move everything on main to a "legacy" branch and have a single merge commit start what ends up being the production version of main. Even with a script, it's still error prone and a little scary to me...
The combination of both makes rebasing completely unusable for teams largely comprised of junior and intermediate developers - your seniors will spend an awful lot of their time helping juniors to revert git rebase fuck-ups, which is why I've seen it banned in quite the few places. We got enough of our own crap to do without having to deal into what exactly the juniors did prior to noticing they've screwed up.
IMHO, it's therefore not surprising to see Facebook using rebase - they have money in abundance, they don't care about some senior dev wasting half a day wrangling with basic tooling.
Arguably, you could achieve the same result of a trunk that looks clean and linear, if the tooling just had an option to hide the feature branches, and show you only the merge commits. But you could also keep the possibility of looking inside the old branches and their individual commit messages, if you wanted to.
(through with some additional metadata during reviews and similar)
Whether the unit of change is a commit or a pull request that gets squashed is irrelevant, what matters is that the resulting history can be understood & bisected, that changes can be reverted, and that changes can be cherry-picked.
Work-in-progress changes should be able to be backed up. This doesn't have to happen in the version control system, but it can, and if it does this should not interfere with the goals of atomic changes.
When talking about VCSs in general, the word “branch” is used to mean a linear sequence or chain of versions connected by a parent/child relationship. Unsurprisingly many git users also have this mental picture of "branches" in git which causes confusion - but it's is not really the fault of users.
For example, if the current main/master commit in git has two parents (which it will have after a merge) then it is not possible to know with certainty which of the two parents was previously pointed to by main/master and which came from the feature "branch". Thus git is unable to present a picture/log of the main/master branch as a linear sequence of commits - all it knows about main/master is what it currently points to. So when you log, it has to present the entire tree which means mingling up commits which were master/main commits and which represent work on feature "branches".
With other "true" branching VCSs, a branch identifies an actual sequence of commits and is not just a pointer to a single version. Thus you can navigate the history even after merging. You can ask it to only show main/master commits or only show commits created on a specified branch. While once a git "branch" is advanced or points to a new commit/version, the fact that it previously pointed to a specific parent is lost.
So git "branches" can only identify a linear sequence of versions if every commit only has a single parent. And this is what the rebasing workflow gives you. And this is why the rebasing workflow, dissatisfying as it is, provides the least friction when using git.
If you use merges, then you have commits with multiple parents and now your main/master _pointer_ can no longer be used to identify a single path/sequence of commits. If main/master points to a commit with two parents, git does not record which of the parents came from main/master and which represents work which was, at one time, pointed to by the feature branch.
So while I prefer VCSs with true branches so that merge workflows can be supported but I've given up trying to use such workflows with git - it's just not supported in a useful way.
Going through the commits one-by-one or just looking at the entire diff both work just fine in most cases. In the former case, the commit messages (even if short one-liners) actually help understand the story of how and why the changes ended up taking the shape they did, so it's usually actually easier than just reading through one big diff.
> To me it always seems the main consideration deciding on commit size is about being considerate of the reviewer. Don't want to harass them with huge commits but also don't want to send barrages of tiny uncontextualized changes.
Commits should be atomic. Their size is irrelevant to that consideration. An atomic change may be one character, or twenty thousand lines (if those changes constitute an atomic (= singular and indivisible) change). This usually doesn't result in a barrage of tiny changes that are difficult to understand on their own, but even if it did, commit messages, the sequence of commits, the full diff of the MR/PR, as well as the attached information on the issue tracker you use (which you presumably have if you're doing code review) all provide more than enough context.
- first I review the commit list (including commentary),
- then I review all the diffs in one go unless it turns out that it's better to use a different approach, in which case I review all the small commits, then all the large commits.
main <- PR #1 <- PR #2 <- PR #3 <- PR #4
You can review them in order where the diff view in the PR shows chunks of changes logically grouped together and can be commented on/amended separately. Once everyone is satisfied with the patch set #4 is pulled into #3 is pulled into #2 ... and it shows up in main as 4 commits each with an independent PR history attached.
Or at least it is with bitbucket. If the first PR is instead merged, the commits vanish from the start of the sequence in the other PRs.
git diff ^initial-commit~1 final-commit
However for that to be available after a contribution has been included in the master branch, one has to use merge commits.Otherwise there are no inherent markers as the graph has been flattened in to a linear sequence. One then has to make use of external tools, or commit comment conventions to recreate what was actually done or intended.
Since branches are effectively free, one "trick" is to backup your branch as a new branch (e.g. "git branch -f my-feature-branch-bk") before starting any voodoo^Wrebasing.
There's also the tried and true fallback: https://xkcd.com/1597/
> Even with everything done right, a rebase-centric workflow will flood your GitHub PR timeline with force push events. These force pushes happen even when you've merely rebased your changes onto a newer trunk, leaving the diff unchanged.
Yes that's the price you pay for a linear history.
For a high churn PR there's no need to rebase on every push, if the PR itself is still in flux and/or you're pushing purely to leverage the centralized CI system, it's fine to tack on additional "wip" commits without rebasing.
It's only the final product that must be rebased to get that silky smooth linear commit history. And you should be doing that anyway so that the logical changes in your PR are unified into a single cohesive unit.
squash merges, by contrast, are a terrible idea. unless for some reason you make commits with comments like 'another change' or 'try again', maybe because your ci system needs a commit to work from. those should be squashed. but squashing a whole branch means that the refactoring work you had to do is mixed into the same commit with the actual functional changes, which makes it much harder to read the commit later. also, obviously, giant commits in your history cripple git bisect
I love that title.
I don't care how the other developers came up with the solution, I want their changeset neatly tagged in the commit with ticket reference.
On a local branch? Definitely rebase. On a branch from an active pr? Definitely merge. Most situations are not so straightforward (what about a pushed branch without a pr? And with a draft?) But in those case mostly depends on the team. If you know/think someone used/reviewed that branch, never rewrite history. Otherwise please do.
In practice too, I also almost always choose rebases, BUT there are a few caveats here that are worth making…
As a general rule rebases for more than a small number of commits in cases where conflicts are non trivial can take significantly more time and patience for the developer performing it. Instead of fixing the conflicts in one swoop, you have to sometimes resolve conflicts repeatedly in the same areas of code. If the conflicts are extensive this can be a very unpleasant experience even for experienced developers.
For less experienced or less skilled developers each of these conflicts poses an opportunity to mess up. Since there are more of them when rebasing, there are also more opportunities to mess up. If the developer is in a rush or is simply impatient, I would also expect them to mess up more in a rebase than a merge.
Yes, ideally we wouldn’t have massive branches that need to be rebased with many conflicts, but in practice if you are working on a codebase where a lot is happening frequently by lots of people in the same parts of the code or code is moving around, there are often no really good alternatives. While perhaps more changes can be made incrementally than people often admit (especially if you include feature flags and the like to allow for transitions — though those can come with their own pitfalls), in practicality, not everything can in practice be done without having longer branches (and not having unreasonably large changes in a commit). Sometimes large changes just need to be brought in together.
Slow reviews or at least slow code reviews at the same level of effectiveness in the review can pose problems as well. If you want it properly reviewed in the safest possible way you really want no new code in the branch being merged to after the review begins — allowing a clean fast forward merge from the rebased branch, but that can be impractical in some codebases with a lot of activity.
Merges can still be messed up and until recently it was quite hard to even see the merge diff to debug a bad merge, but there are admittedly fewer steps where you can mess up.
All that said, I’m still a huge advocate of rebasing, but I get it that not everyone is up to the task. Sadly most developers have tons of awful, broken, and incoherent commits on their own branches that they never clean up. Until you can convince them to at least do fixups and cleanup the commit histories on their on branches with rebasing, arguing for rebasing over merges towards trunks is pointless.
It requires more from the developer upfront. If your developers are good it will require much less from a maintenance perspective but the reality is that having a team of capable developers who are willing to do the work is not something one can take for granted in most places.
And squashing should never be preferred by anyone if the tooling was actually any good. For PRs? You should be able to define a group of commits and look at it as a whole, not just operate on a single commit. For clutter? You should be able to "zoom" in and out, or collapse/expand branches in your UI. For disk space? If the size of each commit was O(size of change + small constant), then squishing wouldn't help, because you'd just be cutting out a few bytes per commit -- assuming you're not thrashing back and forth with your changes. The fact that squashing saves so much space just shows how inefficiently Git is storing things.
We're all suffering that Mercurial lost the war. Of course, even Mercurial isn't perfect. Why did we stop innovating on source control systems a decade ago?
By that school of thought, a repo should be recording what happened, not what you want to look like happened.
I haven't found it to be terribly useful though. IME, it adds noise for future code archeologists.
In very rare cases, having the messy long history can be slightly more useful for bisecting to find precisely where bugs were introduced, simply because it's more fine-grained.
I don’t think anyone needs to see the hundred incantations of `git commit -a -m fuckfuckfuck` I do before I make a PR.
To those who say “you should make meaningful commit messages” I say “yeah that’s what you see in the single commit when I make the PR”. If you say “you should just not commit until your local work is PR worthy”, I would say you’re not using git effectively.
By that logic you should leave typos in, because, you know, it's "more honest". Or you should not edit yourself in any way, because it's "more honest".
The reality is that not editing our communications, or our commit history, is a lack of respect to others, especially when that lack of editing imposes a higher cognitive burden on others (e.g., having to sift through more hay to find needles).
I didn't mean to imply any moral or dogmatic concept of "honesty" here. It only means there is less guessing about what events actually transpired.
But as you say, I agree it most often just adds noise, in my experiences.
But critically, that noise is a lack of respect towards others.
Mainly because it's more work. But it's work worth doing. It's also enjoyable.
To rebase I take your patches, apply them, then I apply my patches, fixing any trip ups. You "got in" first, so I get to do the work.
It seems simple to me - far more than hoping merges do what we both meant.
Conceptually, it's patches all the way down - anything else is to me suspect ... might be me not being big brained enough :-)
Firstly it's only a codebase I have never seen before because I am not keeping up with the changes. Ideally i will pause during rebase carefully read the commits which will include long thoughtful messages laying out important implications, and my tooling will make clear where the conflicts lie, and how test coverage is affected.
We won't ever just grep for <<<<<< and try and remember which HEAD means - is that mine or yours?
I am making changes against an evolving codebase. Either I am talking with people editing that codebase and we are in sympathy or we are hoping a VCS will solve our political and organisational problems.
I honestly think having time to argue about the best way to solve a problem amoung a team is the best solution - let the team talk argue and decide and they will barely need a VCS. If you cannot "spare the time we must deliver" you got other problems
Edit: Look honestly it's all patches. That's the atomic level. And to my mind, having you gather the freshest version of the codebase then apply your chnage son top is the sanest cleanest approach.
The same merge conflict resolution applies - I agree. (basically line conflicts). But what's the option? Anything else gets weirder faster (seen some trying to understand conflicts in functions, etc)
It's local history that one rewrites, so there's nothing "insane" about it. What your comment does is expose your lack of understanding of this matter.
> We're all suffering that Mercurial lost the war. Of course, even Mercurial isn't perfect. Why did we stop innovating on source control systems a decade ago?
Mercurial supports rebasing and history rewriting just fine.
I don't understand this. You do not back up your code to any shared repository while still working on a feature? What if it takes 2+ weeks? You have 2 weeks of code replicated only locally on your laptop? Or what do you mean by local?
If the project is too large for this, you can also fork off the repo itself and then merge in your changes to the main canonical version when you're ready.
If a person commits a lot, they can end with all kinds of little commits which are just fixing typos or syntax errors, exploring half-baked ideas, can't be bothered writing a commit message right now, etc. What's the point of keeping all that stuff long-term?
Then, when it is ready to be reviewed, you rewrite that real history into something more logical - e.g. first commit creates an interface, second commit implements it, third makes other parts of the code call it, etc - and then submit that for review as a PR - that "fake" history is likely to be much more useful to others (and to one's own future self) than the real development history was
back when I used hg small commits were not a problem as the branch name remained after the merge and so mostly we were able to treat branch names as the squashed whiae still having acces to the typo commit if needed.
I don’t think the real issue is Git’s features. Even if Git had whatever additional features you wish it had, it still doesn’t change the fact that a history of typos and stupid mistakes isn’t very useful to anybody (especially in the long-run), and hence there is little or no value in preserving it; while a faked history which breaks the change down into logical steps does have some real long-term value, in terms of helping anyone who needs to review and understand the change in the future
It doesn't matter what source control system you hand me. It could be your ideally perfect system. I would still rewrite history that I'm not collaborating with others on. I try treat source history (as much as is practical) as a sequence of logical changes to the code. I treat it like I treat the code. I try to optimize for making it intelligible and understandable to other humans (including myself).
You can look at any of my projects on GitHub for examples.
So I think what people are saying (and I largely agree with them) is that it's fine to rewrite history as long as you never share the ugly mess that existed before the rewrite with others.
A typical example of this is having un-shared ("local" - but could be backed up) streams of "tweak a comment, fix a semicolon, xxx" changesets that aren't really going to be useful to other people. Before you share with others, you squash them down to a single (or few) "implement feature X" kind of changes, often with linear history.
Having that less-branchy and less-noisy linear history available is useful in the future when you want to examine the history of the repo. Future people don't need to know that you fixed a comment, added a semicolon, etc. They want to know the big coherent logical changes that were applied to the repo.
Squasing/rebasing/fixing history before you share it allows you to have your cake and eat it too, more or less.
----
EDIT: Here's a decent email from Torvalds on the subject (not that I agree w/him about everything, but in this case): https://www.mail-archive.com/dri-devel@lists.sourceforge.net...
Basically it's the equivalent of bringing patches up to date before upstreaming them. Upstream isn't going to apply a patch that no longer applies cleanly.
Local != not pushed to a remote. You can totally push to a per-developer remote, or to branches under a per-developer namespace.
And to answer your actual questions. Yes, you either:
1. Push it to a shared repo but with a name that makes clear it is a branch belonging to you (e.g. `<yourname>/branch`), or
2. Back it up with your standard laptop backup (the one you have set up that backs up non Git stuff too).
> to any shared repository while still working on a feature? What if it takes 2+ weeks?
A lot of projects don't want our partially implemented changes, if only one person is working on them, just the first ready-for-others result and subsequent smaller fix updates.
> Or what do you mean by local?
This is key: remember that git is distributed, so backups of your really local repository should be as easy as pushing to a remote copy. If you aren't doing that (or even if you are) keeping an automatic off-machine backup of your working directories is a good idea in case of drive failure.
If you are working on a feature with a team, perhaps a team-local shared repository is a valid answer.
Squashing PRs on remote is quite common.
You could also do without the disparaging comments.
You're right.
But Meta's "hg" wasn't really FOSS hg for a long time and looks nothing like it today. It started as hg but was hacked to become what is now sort of Sapling (+ Mononoke + Eden + Buck + Phabricator + Sandcastle + Landcastle) integrated together.
The lore was that the git people didn't want to help improve monorepo performance, but the hg people were somewhat more interested so that was the reason behind hg. Then later, Microsoft got git people to upstream changes to accomplish similar performance improvements.
IMO, efficient monorepo dev cannot happen without an SCM-integrated synthetic FS. Also, efficient dev cannot happen without dependencies-aware testing and build caching. It further helps if dev tools are tightly integrated together and documented. Meta didn't do documentation well and didn't name things all that well either. Most internal tools also lacked man pages and generally depend on in-group knowledge hoarding rather than self-discovery and principle of least surprise.
Every now and then, some critical tool (to your workflow) group would come along, break their tools, and leave them broken for weeks with a flimsy excuse like "Oh yeah, our news people are rewriting it in Rust" rather than reverting to a working build.
people rewrite their history because theyre irresponsible with pushes. it's got nothing to do with the source control system. You're supposed to push small isolated encapsulated commits with a good names. I never do that while developing. I would have the same problem on any VCS
> And squashing should never be preferred by anyone if the tooling was actually any good. For PRs? You should be able to define a group of commits and look at it as a whole, not just operate on a single commit. For clutter?
once again responsible commits are supposed to be small, isolated, single change commits. so in a large feature a good developer would have tons of commits this makes the PR easier to understand and review... and when the features finally merged, you don't necessarily need those commits you just need the feature.
I think your issue might just be with understanding how version control is supposed to work.
This has nothing to do with how it's "supposed to" work, and all to do with the standards and constraints we're putting on ourselves here. No god has made any commandments about how thou shalt organize your commits.
> I would have the same problem on any VCS ... commits are supposed to be small, isolated, single change commits
It sounds to me like you've got some self-imposed issues. Nobody told you every commit has to be a "single change", unless you're very loose with what you mean by that. Sure, a commit should be a group of related functionality, such that you either want all of it or none of it. But it's perfectly fine for one commit to involve 20+ files. In fact one of the (many) things people hated about CVS was that you couldn't bundle changes like this! And if you're that concerned about commits being single changes, then you should never squash! It's hypocritical to say every commit should be tiny, but then squash them!
> this makes the PR easier to understand and review
I already addressed this. The problem is the 1:1 mapping between commits and PRs. There's no reason for that.
Its a good ideal but not realistic.
Corporate policy often is to push at the end of the day, so in the event you spill some coffee over your laptop, all you'll lose is a day of lost work and a day to set up a new laptop.
The way I use git, it's not "history" until it's in main. Up until that point, branches are mutable sequences of patches that may or may not ever get merged in.
These patches bear little resemblance to the actual sequence in which I developed the code: as often as not, I'll amend a commit rather than creating a new one, because the point of the version control system for me isn't to faithfully log what I did, it's to represent my change as a discrete sequence of steps to get from main to main+feature.
Viewed that way, rebase is obviously the preferred way to integrate main->feature branch. Main has changed under me in some way that causes a conflict, so I need to reevaluate my sequence of patches in light of the new information.
When it comes to integrating feature->main, I don't have a strong opinion, except that squash is terrible because I specifically designed the commits to make sense as distinct steps.