Instead of
`git push --force`
always use
`git push --force-with-lease`
https://git-scm.com/docs/git-push
This probably should be the default in git (as in there should be a `git push --force-without-lease` instead) and asks git to make sure the commits locally on your branch are up-to-date with those on remote/origin. It then fails if you try to overwrite commits that you haven't seen, and has saved me a few times when working between computers on the same project when i could have lost history on the remote that i failed to fetch.
If you are sure that the repo you are pushing to is a stable target (nobody else is accessing it), you just use --force.
If the repo you are pushing to is a moving target, you ... don't force push to it. Or else you warn all the repo users that you are about to rewrite history. Which means they not only should refrain from pushing, but have to be prepared for a second announcement which informs them that the rewrite is done; they must then fetch the rewritten head and fix up their unpublished work against the non-fast-forward change.
Now it may be that --force-with-lease allows you to sneak in non-fast-forward changes without losing newly introduced upstream changes: but that assumes it's a good idea to be doing that sort of thing without communicating with your team. I.e. as long as we can sneak in a non-fast-forward change without accidentally/unknowingly deleting anyone's work, we are peachy; no need to coordinate.
--force-with-lease exists for the scenario where you are sure, but wrong.
All of this still overlooks the fact that the changes are not lost. Say someone (like one of the personalities in your head you don't know about) publishes a change which you unknowingly clobber with a "git push --force". That someone will notice when they fetch the repo: it has diverged from their clone and when they look at the history of master vs origin/master, they will see that their commit which they are sure they pushed does not appear in origin/master.
If you have multiple downstream checkouts and manage to clobber something with force pushes, you can recover. Then have a word with yourself and work smarter going forward.
Already, "git push" stops from clobbering: it warns that your proposed push is non-fast-forward and that you need --force.
If you are using git as intended, that's all the warning you need.
I understand that there are dubious workflows out there where a repo has multiple downstream users and they are all doing "git push --force", without coordinating with each other. They need a double force to make sure that they are clobbering what they think they are clobbering.
If that's not you (which it arguably shouldn't be, and certainly isn't me), you don't need to know about force-with-lease.
The only thing I would ever do with --force-with-lease is go "oh", and immediately repeat the command with --force, knowing that I'm in a situation in which the check is not applicable.
Even if the force were erasing a new commit that came from another repo, I would know that. Like I pushed something into upstream U from repo B, but I'm fixing the situation out of repo A which hasn't picked up the change. Yes, I know what I'm doing, that's why I'm using --force, and don't require --simon-says-force. I want the chain of commits I now have in A to be exactly what is in U, as a rare exception to normal git use. After I'm done from A, I will switch back to B, do a "git fetch" and probably "git reset --hard origin/master" to make B look like U.
Using --force-with-lease means the git push -f will succeed if no-one's pushed in between, and fail otherwise.
I do that a lot to avoid commits mutating mid-review, so you avoid having to force push over reviewed commits (which is a sin)
The PR reviewer isn't the only person who will ever review your code, you know.
If you police atomic feature branch PRs instead of atomic feature branch commits, though, things actually work out OK.
(By the way, the even more compelling next step to your argument is: pull requests aren't artifacts! I've worked on projects that have emigrated from GitHub, and was left with just the commit log)
How? Usually I rebase the same branch multiple times onto different, but successive commits of the master branch. But after I solved a bunch of conflicts of the first rebase, I shouldn't have the same conflicts again in a second one, since the rebased branch contains the merged conflict. Rebasing again could only turn up new conflicts (with newer, other commits on the master branch).
How can I have the same conflict again for repeated rebases?
What then happens is that when a bug is found that affects all branches, it must be cherry picked into all of them. If that cherry pick runs into conflicts, it is often the same conflicts, over and over again on each branch.
Of course, the fix is not to do that, but it's easier to say that than to get away from that kind of workflow once you are steeped in it up to the chin.
I worked at a place that did this.
The code was written in C, and I always thought the better solution would have been to use #define/#ifdef to flag certain blocks of code out of the compilation.
A branch for each product was a nightmare when there were 10+ products, some with multiple variations, each on its own branch. Backporting a bug fix meant cherry-picking into 20+ branches. What made it especially stupid was office politics from each product having its own PM, and then the PM for one of the products would decide the bug wasn't significant enough to spend the time doing the cherry-pick and testing. This happened too often when it came to security fixes when a PM didn't understand the issue.
The benefit that everything is integrated, so there are no games with having to cherry pick things this way and that and losing fixes.
The apparent downside is that there is no isolation. Inside some of those #ifdefs is code that you are not building. But changes you are making can break that code for someone else.
While this may seem risky, it's actually better. Breaking something now is better than someone cherry picking your fix 8 months later into their branch and then dealing with the breakage.
Fixes to common code never get left behind; everyone working off the trunk instantly gets them.
The #ifdefs are immediately and constantly visible, telling you where the code is that is or is not part of what you are doing, and reminding you of its existence. They greatly discourage refactoring parts that you cannot test.
There is pressure to keep those #ifdefs clean, whereas people go hog wild when they have their own branch, thinking they can rewrite whatever they want to suit what they are doing.
git checkout feature
git rebase main
git checkout main
git merge feature
that way you get all your conflicts on the feature branch during rebase and your merge is always clean.
Pretty sure it's the other way around. You're on the branch and rebase it atop current master. If you merge after that, you won't have merge conflicts.
Let's say in my feature branch in the first commit I change a line in a file which also gets changed in the main branch. Then I have done 3-4 change commits doing changes in the same file. Now while doing rebase, I will have to resolve this conflict 3–4 times again and again while git re-applies commit one by one, during rebase.
I think I get this sometimes even if rerere is enabled, I am doing rebase using Intellij though, so maybe rerere doesn't get used here somehow or maybe diff context changes, so rerere is not applicable.
What I would check is hooks, or any other customizations. Especially on Windows, data loss is absolutely possible with misconfigured hooks, but it has nothing to do with when a commit was made.
https://stackoverflow.com/a/28510260
That completely fixed the problem. Now I only ever get conflicts on my feature branches. The rule is always: rebase away from main, and merge towards main. All conflicts are then on your branches, never on main, and always from rebase, never from merge. Then I set the pull behaviour to rebase, too.
I've never had a silent regression since, and never had a problematic conflict scenario.
I did recently learn about ORIG_HEAD though which was very cool, because I had accidentally rebased to main instead of to a dev branch from which I had created a bunch of worktrees, then when I merged back to the dev branch all hell broke loose, and I learned that I could revert a merge by checking out ORIG_HEAD:
https://icinga.com/blog/undo-git-reset-hard/