MgSam
MgSam

Reputation: 12803

Git squash- no merge conflicts

We have a feature branch that people merge into from their personal branches. 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.

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?

Upvotes: 1

Views: 496

Answers (3)

Woody
Woody

Reputation: 844

git rebase -i <hash of the commit used to create the feature branch in the first place>

You will get no conflicts because your rebase your feature branch with the initial commit used to create the branch.

During interactive rebase use 'f' for fixup for all your commits (except the first one), it will squash all your commits and discard their commit's log message it will look like this in command line

pick f0454e5 [RFG] window parameter generic
f 4eca527 [FIX] adding missing column
f 13f5202 [FIX] table name

Upvotes: 0

torek
torek

Reputation: 487825

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.

Upvotes: 2

hspandher
hspandher

Reputation: 16733

You can use merge --squash

git checkout <your_branch>
git merge --squash develop

git commit

However, be aware that, irrespective of the name, this approach doesn't actually result in a merge. It just brings all the changes into your working index and you can commit them accordingly as if you've done them in the target branch, to begin with.

Upvotes: 1

Related Questions