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

The reason to squash commits is more than just keeping your commit history read-able, it's about making easy to revert a feature and being able to keep history in a way that makes it simple to revert a change if you run into issues.

If I rollout a rewrite of an endpoint and run into a weird issue in the QA environment, I'm a simple git revert away from fixing the issue. If I had spread that endpoint across 25 commits, I'd have to actually debug the issue in QA and figure out what I broke. Reverting quickly lets me debug the issue on my time instead of keeping our test suite broken.

It's not always feasible, and I try not to be a stickler when people on my team don't do it but the fact is, if you're working in a world when you're delivery code quickly into real environments, having a back-out strategy is paramount.



Programmers can have the best of both worlds. Use granular commits on a local branch and squash merge into shared branches.

That way one gets clean shared history while preserving local work history.


The problem with this approach is the local branch is no longer represented in the shared branch. So if I'm working on a larger feature and want to PR an intermediate part and continue working, I'm in for a bad merge.

If I want to merge my hotfix topic branch into both the release and the master branch, their commits won't match so I can't check if it's present in both automatically.

If a topic branch is left up instead of deleted after a squash merge, I can't even see that master is ahead of it!

Squash is an ugly hack that creates as many problems as it solves.

I desperately wish git had a "group commits" feature that let me manage a cluster of related commits as a single commit for the purposes of history-viewing, reverting, and cherry-picking.


> I desperately wish git had a "group commits" feature that let me manage a cluster of related commits as a single commit for the purposes of history-viewing, reverting, and cherry-picking.

Merge commits work fine for most of that, you just have to adapt to merge UX/commands:

--first-parent (to git log, git annotate, etc) gives you clean history viewing of just your "groups" (your merge commits).

--first-parent even works for bisect allowing you start by figuring out which merge commit brought in a change (and then dig into the merge branch itself if needed as a second bisect).

You can revert or cherry pick merges if you provide the -m (mainline) flag to tell it which parent to consider the mainline (usually the first parent, but not always depending on your intended revert/cherry-pick; it complicates what you need to know about the revert/cherry-pick, but if you are in the process of revert/cherry-picking you should already be figuring out what your mainline is and expecting some possible complications).

I think sometimes the only "problem" with Merge commits is too few pretty UX tools default to a --first-parent view of the git graph and don't themselves provide good tool for picking that -m (mainline) for revert/cherry-pick.


This. I hated working with merge commits until I discovered --first-parent. I'm not sure how it evaded my attention for so long.


I think it is an interesting tools problem: making interesting graph diagrams of the full git log graph is a fun and interesting problem and often looks great in screenshots, but rarely is a particularly "useful" view to most users. It's not as "fun", doesn't produce as many shiny/colorful screenshots, to build --first-parent views by default and introduce (and test) graph drill-down based user experiences.


> I desperately wish git had a "group commits" feature that let me manage a cluster of related commits as a single commit for the purposes of history-viewing, reverting, and cherry-picking.

Maybe you can do something similar to that with git-replace?

Otherwise, you could just tag your commits by putting something in the commit message and use `git grep` to search for those commits. Then you could just build a small helper script that 1. greps for a tag 2. loops over the found commits 3. rewinds them or whatever and 4. squashes the resulting commits.

Dunno, there's a bunch of other ways depending on how exactly you want it. But I agree that grouping commits would be pretty cool :D


But doesn't that assume that you based your larger feature on the local branch, instead of the squashed public version? If you wanted to build on the previous commit, why wouldn't you build on the squashed version?


> If you wanted to build on the previous commit, why wouldn't you build on the squashed version?

alexmingoia proposed:

> That way one gets clean shared history while preserving local work history.

But if you keep on building upon the squashed versions each time you do a merge, you won't have convenient access to the local work history any more.

If you wanted to have access to the local work history you'd need to keep each branch alive still, each time based on the squashed history plus your individual commits up until the next squash.


I suppose you could cherry-pick the new branch onto the squashed version, but I'm still not sure of the value.

You have the local work history already, and that work is done to the point of it being squashed and pushed. Why would you need to keep referencing it to the point that it's inconvenient to have it in another branch?

> you'd need to keep each branch alive still, each time based on the squashed history plus your individual commits up until the next squash.

exactly what I do, and there are maybe just three or four branches I maintain for reference, very few squashed branches are useful a couple of weeks after they are released.


> I desperately wish git had a "group commits" feature that let me manage a cluster of related commits as a single commit for the purposes of history-viewing, reverting, and cherry-picking.

Isn't this what git does by default when you merge your changes? The merge commits group small commits together.


> if I'm working on a larger feature and want to PR an intermediate part and continue working, I'm in for a bad merge.

I usually deal with this by `git rebase`ing your feature branch on top of the shared branch as soon as the PR is merged. You sometimes still get merge conflicts with this approach, but they're always in the code you've just written so they're usually pretty easy to fix.


The problem with rebasing is you might break every one of those commits, which defeats the purpose of chunking the work like that in the first place (since its no longer an honest reflection of what was happening/what worked at each point).


I usually rebase and then squash so that becomes a non-issue. I often find I want to commit more often than the code is in a working state, so I like to be able to erase that history later. I try to keep the whole branch small enough that it'a fine being in one commit.


I don’t really like squashing on merge, since it destroys the commits I may have made. If you’re getting a PR from me and it has more than one commit, I have done that on purpose and you want to keep those separate.


> squash merge into shared branches

Why not just rely on a merge commit instead?


My gripe with merge commits is they don't integrate nicely with `git blame`. If I'm looking through historic commits (to understand why a change was made, or perhaps to debug an issue) I'll often `git blame` the line and diff that commit. If the commit is super granular, I can't get the context of the whole change: I need to dig for the merge commit then look at that, which is faff.

If there's a way that I don't know to show merge commits in blame rather than the actual source change commit, then I'd be all over it. Until then, single (whole) units of change per commit.


`git blame --first-parent` will stick to merge commits in blame output.


Why would you want one author for a merge commit? That merge can have many commits by many different authors.


They can, but

1. The large majority of PRs I've reviewed have a single contributor. Additional contributors are rare. When they do happen, they're often a minority contributor or simply consulting on a PR. It's net neutral when all PRs are squashed in the same pattern.

2. Even with multiple contributors, most features have one leader. It's much easier to talk to that person (and have them delegate) than it is to piece together multiple contributions.


That's an interesting point, but it seems like one among many very good arguments for "right-sized" or "logical" commits, i.e. not 10 similar-shaped bugfixes and also not one-line no-context diffs -- how big should the PR and merge commit be though? Maybe you put the 10 similar-shaped bugfixes together into one PR because they review together easily, but each fix is its own commit, because they all logically stand on their own.

Your use-case is actually the cause of a common rule I've seen at work of requiring a ticket reference in each commit message, which allows looking up the original ticket and associated PRs, along with any commentary & discussion at the time the commit was merged.

On a big code-archeological dig, I often follow a path like run blame -> look at the diff -> pull the ticket reference -> find ticket in issue tracker -> read its description & comments -> find linked PR #'s in the ticket tracker -> open PRs & read diffs and comments -> repeat for linked issues if needed (and then as often as not still end up baffled)

One team actually kept an old redmine VM instance running mostly based on my personal use long after we'd migrated to JIRA, so... I think my approach may be a little unusual! At the least, doing better sized commits would a huge step for every case involving blame.


I personally really dislike merge commits because it makes the tree really difficult to follow in most visualizations. If I'm trying to follow the main branch only to a certain point the graph is polluted with all the "WIP" side branch commits between head and the commit I end up getting to.

It also defaults to causing the main to have a ton of commits with "Merged from XXXX branch" as the summary lines when that's not nearly descriptive enough to quickly find what type of commit I may be looking for.


> If I'm trying to follow the main branch only to a certain point the graph is polluted with all the "WIP" side branch commits between head and the commit I end up getting to.

Using merge commits doesn't have to mean that you don't squash at all. At one extreme, you include every single commit ever made on the branch, and at the other extreme, you squash the entire branch down to a single commit. Using merge commits, you can go for any option inbetween.

> It also defaults to causing the main to have a ton of commits with "Merged from XXXX branch" as the summary lines when that's not nearly descriptive enough to quickly find what type of commit I may be looking for.

Do you often read the log linearly? 99% of the time when I investigate history in Git, it's either through "git blame" or through "git log -S somestring" (search for commits that introduced or removed "somestring"). I rarely, if ever, just read the log as-is.


Use —first-parents and you get a log of only the merge commits themselves, a flat log


I would equally argue there is nothing wrong with the local work history branches being remotely available. If I have multiple branches for one ticket nobody ever seems to care, they just care about which branch is in the PR. Besides, if you have a reasonable web UI for git, it shows it all merged together as one big changeset.


Yes


A merge commit works just like a squashed commit except it keeps all history. This is precisely why it's sensible to avoid fast-forwarding since that operation discards the fact that a branch existed in the first place.

It's better to always have merge commits. They can be reverted just as easily. I don't understand why merge commits aren't the default in git.


If you can fast forward the fact that the branch ever existed was irrelevant, since the branch is a direct child of what you based it on. Just with a different name.


It's not irrelevant. The branch represents a feature, a topic. It groups the commits you're merging into one logical set. This grouping of commits is exactly what will let you revert the feature later if it causes problems.


I've always been frustrated by losing my topic branches once they're merged and deleted, but can't bring myself to clutter my local branches keeping them around, to the point of tagging them just to keep track- I like the sound of your argument and will look into how fast-forward effect my commit history vs. a merge commit, thx c:


This is where you realize that what's killing git is that git has no concept of branches whatsoever. Such a merge isn't "merging A into B (plus shove metadata as string into the commit message)", it is "merging A and B together", which is topologically identical, but semantically very distinct.

That's why Mercurial (esp. with evolve and topics) has forever my preference over git.


This.

I can't figure out how to explain to Junior Devs (who have only ever known git), that they have a concept of a branch in their head that doesn't match the concept their tool of choice is giving them.

We talk about "branches" as logical sets of changes. We give them meaningful names, we construct the concept of Pull Requests and code reviews around the concept of a branch. We later refer to Feature X as having landed in master from branch Y. But git doesn't have any of those semantics. It has lots of ways of dealing with commits, and a facade of a branching model is just one more way of dealing with commits. Branches are not a first-class concept in git. And certainly not like they are in our minds.

However, git is amazing at what it does! And if I was running the world's most popular OS kernel development team and was expecting to receive hundreds of patches a day via email from developers in whom I have limited trust, I would definitely start with git's model and change the way my brain works to match its semantics.

Instead, I find myself on a small team of high-trust coworkers who all talk about branches as if they really exist in our git history, and somehow I'm the crazy one for pointing out that every time we hit a problem with this mismatch the fact that we're using git is the reason that we can't have nice things.


That's not completely true. There's an order to the parents of a merge commit, and by convention the first parent is the one that was merged "into".


So if I'm on master, and 'git merge feature-x', then the parent commit on the master branch will be the first parent? (Not the feature-x last commit)


Correct.


Thanks


Indeed it is just a normal commit that has extra parent commit…

Git commit is always a standalone item that linked to parent commits. The actual content can be completely unrelated to parent commmits if you must do.(but what is the point of doing this?)

A merge commit is just a commit that has more than one parent.


> I'm a simple git revert away from fixing the issue.

I don’t think this is true /just/ for squashed commits as you can simply revert the merge. Squashing is also bad, because you’ll lose the history of the bug fix that prevented the deployment. Or at least someone will have a hell of a time deciphering the PR to reintegrate the original change and your bug fix.

After a few days, weeks, or months, the argument loses even more water because code will likely depend on the commit in question.

War story: there was a time someone accidentally deleted a multi-gb table in production. The table would take hours to delete and replicate globally, so the entire company spent at least an hour deleting the feature from production to stop the database errors. There wasn’t any reverting of original commits. No one had time for that.


> If I rollout a rewrite of an endpoint and run into a weird issue in the QA environment, I'm a simple git revert away from fixing the issue

I'm firmly of the belief that any benefits that squashing brings would be better achieved with better tooling, rather than by re-writing history and throwing away potential debugging information. In this case, what you need is for git to make it easier to revert 25 commits in one go


How about the case where I made a commit because I wanted to switch to my laptop and continue working? Why should I keep that around ok my history?


It was clearly a natural stopping point in that you stopped working on the code for the amount of time it took to switch. There's nothing wrong with encoding natural stopping points in your commit history, even when those natural stopping points don't always align with semantic stopping points (features/fixes/completed tasks). Sometimes those natural stopping points even encode data you might miss later: sometimes in making that switch you also mentally switch tracks and looking at where you stopped and where you restarted sometimes can remind you of things you forgot in between. I've found debugging cases where that helped me solve problems. (That said, in practice I rarely keep such "natural stop commits" myself, but I've also never been strict about it and sometimes keep them. There's nothing wrong with them and sometimes they are useful "waterline" markers in a larger effort.)


I don't make these commits often either, they exist almost exclusively to move code across machines. Actually, I commit much less frequently than most people, keeping most of my work in the staging area for long periods of time until it's ready to be committed. Probably a bad strategy, but it keeps my code clean…


My point is no matter what your commit strategy is, the reality of what actually happened is debugging info, and worth preserving to help debug later. While debugging and digging through history I'm often asking the question "what were they thinking?", and the info that you made a commit just to move to a different machine might make me e.g. consider it more likely that you left something out, or stopped a line of development early because you needed to do the commit earlier than you normally would. Squashing tries to hide the "how" and "in which order" so you only get the "what" of what was developed. But "how" and "in which order" are valuable pieces of information too.


If git has a concept of branches like mercurial has it would be a lot easier as you can actually see what branch your commits attached to.

(there are pros and cons of both approaches - this is a con of the git approach, I'm not knowledge about enough about esoteric details to comment on if git actually made a bad choice or just a compromise)


Thankfully most Git hosting websites do have this concept. You can revert a PR whole using github, etc


If you use the standard merge commit message in git, then you can still tell what branch things came from when being merged. As someone that has used both mercurial and git, the trouble with named branches (and having to remember to remove them when merging to master) is one of the reasons why I prefer git.


Knowing the name of the branch is not enough to find the commits. With Git, a branch is just a kind of moving tag on the last commit.

The problem mentioned in this thread is rolling back a feature that was merged. The only solution I know is navigating the log to find the first commit on the branch from which to revert. Don't forget there may have been several merges from and to master, as well as commits shared with other git branches that should not be reverted.

In a Mercurial branch, each commit is tagged with the branch name. Unamed branches, à la Git, are called "bookmarks".


> The only solution I know is navigating the log to find the first commit on the branch from which to revert. Don't forget there may have been several merges from and to master, as well as commits shared with other git branches that should not be reverted.

The only step necessary to revert the changes from a merged branch is to revert the merge commit. See:

https://stackoverflow.com/questions/7099833/how-to-revert-a-...

https://github.com/git/git/blob/master/Documentation/howto/r...


Also, can't you just revert the merge commit?


Yeah but I’m guessing most people commenting don’t know how. It’s:

git revert -m 1 <commit>

Or something


Like a tag? (I'm a git beginner so no sarcasm here, really I'm really interested in this stuff.)


The problem is if multiple people make other changes after yours. You can certainly go back to your tag, but you lose all their changes as well.

If you revert one specific commit it just does another commit with the exact inverse of what happened in the reverted commit, so all other changes are preserved.


git-revert supports ranges if you've used any version within the last couple of years.


The fix is very easy: forbid fast-forward merges, and then you can always revert the merge commit of particular feature branch.

As for cleanliness of history, everything should be as simple as possible, but not simpler. Squashing and rebasing is destroying history, which often could be valuable, as OP shows.


forbid fast-forward merges?!!?!


Fast forward merges lose track of the fact that these commits you are merging are, together, doing a coherent thing.


For the purspose given that would work. But it feels icky.


> If I rollout a rewrite of an endpoint and run into a weird issue in the QA environment, I'm a simple git revert away from fixing the issue. If I had spread that endpoint across 25 commits, I'd have to actually debug the issue in QA and figure out what I broke.

Surely this is why we have things like release tags and snapshots of previous versions?

Unpicking individual features is rarely simple even if you have got your git repo into an immaculate state, as things are interdependent.


I wish git had a builtin notion of two different types of commits: working commits and release commits.

I really like making tiny, continuous commits as I work. It's a great flow. git-revert becomes a Ctrl-Z on steroids. I don't what to clutter up the "official" history, with all these tiny changes, many of which don't even compile. That breaks git-bissect and all kinds of other flows.

So the only option is to squash commits. But there's something deeply uncomfortable and unsettling about permanently re-writing history. Plus, it's nice to have a history of those working commits as an artifact. If I'm trying to unpack the reason that I did something 9 months ago, then seeing a replay of the code changes is super-useful.


I go one step further and work entirely in get rebase -i where I build up a stack of tiny, incremental commits. This also lets me get small changes reviewed and committed on a nearly daily basis instead of building up days or weeks of work.

I've been wishing for a git GUI that lets me drag hunks around such a stack so I don't have to keep moving them by hand.


You can use merge commits to create your release groups. Use tools like --first-parent to limit the depth you see of the graph by default; that clutter can still be there, but you can take advantage of the nature of the graph itself to "declutter" simply by setting the "depth" you are working in.


You could get this behavior pretty easily with a double merge: “main” which contains the squashed commits and “history” which contains all commits as well as “main” merged back in post-squash.

I do see your point though.

Edit: the only reason to merge “main” into “history” is to enforce convergence.



> two different types of commits: working commits and release commits

??

   git tag
??


You can also have merge commits and revert only the merge commit in this case...

But yes, generally history is much cleaner when squashing commits.


> If I had spread that endpoint across 25 commits, I'd have to actually debug the issue in QA and figure out what I broke

No, you could just revert the entire lot in one go.


that's not a valid argument. How do you identify "the lot"?


There's an obvious upper bound. If you can't identify the last in-prod version that worked, and the first in-prod version that failed, then you have bigger problems than deciding whether to squash your commits.


    last_prod_tag < the_lot < current_prod_tag


And if the merge of their commits interleaves with other commits from... teammates?


Thanks to the complexity of how components integrate, even seemingly disjointed components, their commits are going to matter when it comes to troubleshooting an issue.

This gets a bit more murkey with mono-repos, but even microservices can combine to create complex production issues.


This sort of sidesteps the issue, IMO, of how hard it is to isolate a problem. Yeah sometimes things collide, but pretty often in my experience there has been one change that needs reverting or patching, not six changes that interleaved. Or rather, interleaving them at best increases the difficulty of the blame game.


By story number, by author, by timestamp, by looking for merge commits, by reading the commit messages - by common sense, basically. If someone has just pushed broken code to master, they usually know exactly what they've just pushed. This doesn't seem like a real problem to me.


By the ticket/issue number?


You very rarely want to rollback a major feature. I’ve never seen it done except right after a deployment, in which case you can revert the merge commit instead. Bisect to find a breaking change is a very common operation. Squashing is bad if you expect to work on your project long term or if others may have the same commits as you from working on the same feature branch.


One can branch and then merge features, and get easily revertible commits and a full history at the same time.

Of course, there are caveats for reverting a merge - finding the right parent adds another failure-prone step, and your team can get in all sorts of trouble if they try to work with the branch without reverting the revert.


Wouldn't it be preferable, then, to avoid squashing your commits, and then tag your releases? This gets you the best of both worlds, you can still back out to a known good version in case of issues, and you can still bisect to narrow down the exact commit that caused your problem.

Bisect is the killer feature of git, for me. Squashing releases takes that superpower away.


Completely agreed — squashing commits isn’t desirable in every circumstance, but if all the commits pertain to a single feature then it makes complete sense to me.




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

Search: