Is there a way to squash the entire branch (around 20 commits with various merges into it along the way) and only keep the changes of HEAD vs the develop where the branch was created without having to waste time navigating tons of merge conflicts?
Yes. To understand this, though, along with all its implications, you should start—as you should very often start with many things in Git—by drawing (some part of) the commit graph.
Let's begin with the setup:
We have a feature branch that people merge into from their personal branches.
This means (at least to me) that we start with, e.g.:
...--o--o--o--o <-- master |\ | o--o <-- alice | \ | *------------* <-- feature |\ / / | o--o <-- bob / | / .............---o <-- carol
The *-ed commits here are merge commits—the first one presumably made by either Alice or Bob, and the second probably by Carol, though the actual author and committer are not that relevant.
We like our feature branches to be a single commit based off of develop, so occasionally we squash the feature branch and then rebase off of develop.
We do squashing using git rebase -i HEAD~<# of commits>. The problem is that git forces merge conflict resolution when doing this. But this is a huge waste of time (and error prone) to try and resolve these conflicts, when all we really care about is the state the repo is in as of the most recent commit.
If you do this by doing git checkout feature; git rebase -i ... you are definitely doing it wrong :-) because git rebase cannot retain merges. The rebase command must, instead, enumerate all the commits it will copy excluding the merges, then copy those commits, one at a time, as if by git cherry-pick (and in fact git rebase -i literally runs git cherry-pick). The result is a straightened-out sequence of commits, omitting the merge points: Alice's, then Bob's, then Carol's, or other order but let's just assume we retain Alice's original commits:
...--o--o--o--o <-- master \ o--o <-- alice \ B1-B2-C1-C2 <-- feature
(Note: Bob's and Carol's original commits, along with the branch names pointing to them, may or may not still be available, we're just choosing not to draw them here to avoid cluttering up the drawing.)
If you change all the pick commands to squash commands, what the rebase command does is fold all those copies together into one big copy:
...--o--o--o--o <-- master \ A1A2B1B2C1C2 <-- feature
(and once again, Alice's, Bob's, and Carol's originals might still be in here, with branch labels pointing to them; but the name feature now points to the single commit with all of these combined).
Presumably, what you want is this big combined commit—but you want its source tree to match the commit that was at the tip of feature before. That is, when we drew:
...--o--o--o--o <-- master |\ | o--o <-- alice | \ | *------------* <-- feature |\ / / | o--o <-- bob / | / .............---o <-- carol
the commit to which feature points has the tree you want; you just want to copy this tree to a new commit, to which a new name like new-feature could point:
X <-- new-feature / ...--o--o--o--o <-- master |\ | o--o <-- alice | \ | *------------* <-- feature |\ / / | o--o <-- bob / | / .............---o <-- carol
Here, commit X and commit feature (the rightmost * in the drawing) have different hash IDs and are different commits, but they share the same source tree: git diff new-feature feature will show nothing at all.
This new-feature commit is very easy to make, you just can't use a standard Git command to do it. The command that makes this commit is git commit-tree: you tell it which source tree to commit, and what parent commit you want—you have to find that commit somehow—and it makes commit X. It produces the hash ID of that commit as its standard output:
tree=$(git rev-parse feature^{tree}) # find the tree to keep parent=... # somehow, find the place to put this commit echo 'combined commit for replacement feature branch' > /tmp/msg-file hash=$(git commit-tree -F /tmp/msg-file -p $parent $tree)
The ... part is left for you to figure out. It may be as simple as git merge-base master feature (but beware of multiple merge bases and situations where that's not the commit you expect). It may be even simpler, if you never allow new commits to grow on master, so that it's just "the commit to which master points".
Having made this new commit X, you need to point some branch name to it:
git branch new-feature $hash
and now you have the graph we drew above, with new branch name new-feature pointing to the new feature branch.
You can now delete the branch named feature, which will forget / lose / give-up all the commits reachable from that branch-name. Or, instead of deleting the old branch and making a new one, you can just forcibly update the old branch name to point to the new commit, so that the old commit is in the branch name's reflog and is thus retained for the usual 30 days (after which Git will forget / lose / give-up all the commits as before, unless some other name(s) keep some or all of them alive).
Or, you can rename the old feature branch, e.g., feature-<date>, or make it a tag (make the tag and delete the old feature branch); and then you can rename the new one to be just feature. This way you'll keep, forever—or until you delete the old feature-<date> name—all the commits it remembered, but under that name instead of under the name feature.
Note that anyone who has a copy—a clone—of the repository will have an origin/feature. If they are using that, and you re-point the name feature to point to new commit X, those who have the clone will pick up the new name and the new commit X when they run git fetch. Their origin/feature will stop pointing to the old tangled graph fragment, and start pointing instead to new commit X.
reset --hardbut again, I'm really not clear how this will help