0

I have a branch feature/Install New feature on this branch and screens and having 11 commits. But all commits have merged commits. Planning to do a squash these commits.

I have list of merged commits in a log history and need to squash into a single commit.

commit ac174b8dc1dc44e91b56c89c55003942070b9742
Merge: e9048249 e24218ee
Author: sanjay <sanjay@sanjay.com>
Date:   Sun Dec 10 04:48:39 2017 +0000

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit e904824938f2e8517d3ad5a45a11ae4595157cf7
Merge: 41e1d616 a3128511
Author: sanjay <sanjay@sanjay.com>
Date:   Fri Dec 8 12:07:53 2017 +0000

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit e24218eeb60bcbfa92559cf174d3de40b93a6dbe
Merge: 41e1d616 a3128511
Author: sanjay <sanjay@sanjay.com>
Date:   Fri Dec 8 12:07:53 2017 +0000

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit 41e1d61609ea6d3c99d52efb3fb472a18924b2f1
Merge: ddc36e3b bdf8a179
Author: sanjay <sanjay@sanjay.com>
Date:   Fri Dec 8 09:14:59 2017 +0000

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit bdf8a17968543fccc3b02ffc59c2117448f586ff
Merge: d9fe3abd 7b630927
Author: sanjay <sanjay@sanjay.com>
Date:   Tue Dec 12 14:53:19 2017 +0530

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit d9fe3abd0062475cfdff911ce58a967076d5aa08
Merge: 27ee100a 63113ae4
Author: sanjay <sanjay@sanjay.com>
Date:   Tue Dec 12 14:52:34 2017 +0530

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit a3128511b3fd3746d4191794e7dcda52232e9458
Merge: ddc36e3b bdf8a179
Author: sanjay <sanjay@sanjay.com>
Date:   Fri Dec 8 09:14:59 2017 +0000

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit ddc36e3be2dd55b1ba880c307c8be0237ca52bce
Merge: d9fe3abd 7b630927
Author: sanjay <sanjay@sanjay.com>
Date:   Tue Dec 12 14:53:19 2017 +0530

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit 7b630927be19a773414938a43702fe9cd0e7f854
Merge: 27ee100a 63113ae4
Author: sanjay <sanjay@sanjay.com>
Date:   Fri Dec 8 01:52:01 2017 +0000

    Merge branch 'feature/Install New feature on this branch and screens' of https://github.com/service/dosomething.git into feature/Install New feature on this branch and screens

commit 27ee100ad29e8db7fb10ddc04824ccdc8a53d091
Author: sanjay <sanjay@sanjay.com>
Date:   Fri Dec 1 06:25:13 2017 +0000

    Install New feature on this branch and screens

commit 63113ae404be96f113e1c9eb4f79d0de9fc4a90e
Author: sanjay <sanjay@sanjay.com>
Date:   Fri Dec 1 06:25:13 2017 +0000

    Install New feature on this branch

But I did a git rebase -i branchname it shows output like this. I am not able to squash it a commit. output was like this

noop

# Rebase ac174b8..ac174b8 onto ac174b8 (1 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

How can I approach a squash with these above commits.

Sanjay Dutt
  • 1,173
  • 4
  • 15
  • 19

1 Answers1

0

TL;DR answer

You cannot achieve what you want via git rebase. You may be able to achieve what you want via git merge --squash, but unless you know what you are doing, you should in general not use git merge --squash at all. (That is, this may be the wrong approach to the overall problem.)

As a rule of thumb, after running git merge --squash for some set of commits, you should delete the branch or branches that contain those commits, as they're no longer good to develop on. (There may be specific exceptions.) In this case, that would mean deleting each of the branches that you had merged.

Things to know about rebase

As evolutionxbox said in a comment, when you ran your git rebase -i branchname command, you selected to copy no commits at all, placing the copies after the current commit ac174b8dc1dc44e91b56c89c55003942070b9742.

This is probably a good thing, because when you do select some set of commits to copy, placing the copies after some other specifed commit, the commits that Git will copy omit all the merge commits. This is because it is both impossible, and generally unproductive, to copy a merge commit. Since 9 of the 11 commits in the list above are merges, you cannot copy them.

It's important to remember that what git rebase does is to copy commits, as if via using the git cherry-pick command. In general, you will have a Git repository that has some set of commits in it, and you can and should draw a picture—a graph—of those commits, or at least, those that are to be involved in the rebase:

       E--F--G   <-- new-desired-base
      /
...--*
      \
       A--B--C--D   <-- four-commits

Here, each of the four commits that are reachable only from the name four-commits are ordinary (non-merge) commits. The parent of commit D is commit C; the parent of C is B; the parent of B is A; and the parent of A is the commit I marked with an asterisk *.

If you now run git checkout four-commits && git rebase new-desired-base, Git will, starting from the commit to which four-commits points (i.e., D), select commits that are not reachable by starting from the commit to which new-desired-base points, i.e., G. The commits reachable from G are G, F, E, *, and every commit before commit *. So subtracting that list from the list D, C, B, A, *, ... leaves D, C, B, A.

Git will now switch to commit G, as identified by new-desired-base, and copy each of the four commits, in reversed order, i.e., the most-ancestral A great-grand-parent of D, then the grandparent B, then the parent C, and finally D itself. Each copy is done as if by git cherry-pick.1 If all goes well, the result looks like this:

       E--F--G   <-- new-desired-base
      /       \
...--*         A'-B'-C'-D'
      \
       A--B--C--D   <-- four-commits

where the ' (prime) marks indicate which commit was copied to a new commit with a new hash ID.

Finally, if all has gone well, Git will take the name four-commits off the original chain, and paste it onto the end of the new chain, re-attaching HEAD in the process:

       E--F--G   <-- new-desired-base
      /       \
...--*         A'-B'-C'-D'   <-- four-commits (HEAD)
      \
       A--B--C--D   [abandoned]

Now that there is no name for original commit D, it seems to vanish entirely. This takes the entire chain with it, so A-B-C-D seem to have been replaced with A'-B'-C'-D':

       E--F--G   <-- new-desired-base
      /       \
...--*         A'-B'-C'-D'   <-- four-commits (HEAD)

The original commit chain is still in your repository, retrievable by its hash ID if you've saved that somewhere (and by some special mostly-hidden names in case you haven't saved it), typically for at least 30 more days, in case you change your mind about the rebase.

Note that if anyone else has those commits in their repositories—i.e., has those hash IDs and has their own names for them—those commits will remain forever in their repositories. You must get them to remove their names for those commits, before those commits will eventually go away from their repositories too.


1An interactive rebase really does run git cherry-pick on each commit to be copied. Some other forms of rebase don't, but the effect is normally the same.


What to consider before proceeding

Since that's how rebase works, it is a good idea to keep it in mind. It's even more important to remember that it doesn't copy merge commits, so if you asked it to copy your chain of commits, it would omit the merges. In general, though, it's a bad idea to ask Git to rebase something that includes a merge, because the commits that are selected to be copied tend to include the commits being merged.

Consider the following simplified diagram:

...--A--B----F   <-- master
      \  \
       \  D   <-- feature-B
        \
         C--E   <-- feature-A

Remember that commits are to the left if they are more-ancestral (parents or grandparents), and to the right if they are less-ancestral (children). So master has had one commit added, namely F, since feature-B branched off. It's had two commits added, namely B and F, since feature-A branched off. Meanwhile feature-A contains two commits, C and E, that are not on master, and feature-B contains one commit, D, that is not on master.

If your goal is to make a single new commit on master that incorporates both features at once, you might choose to first merge the two feature branches. You can do this with either:

git checkout feature-A && git merge feature-B

or:

git checkout feature-B && git merge feature-A

The result of the merge, if successful, will be a new commit G. This new commit will be based upon (as in, the merge base is) commit A:

  • Git will compare the contents of the snapshot for commit A with that for the snapshot in D. This is, in effect, what happened in feature-B since commit A. Note that this includes what happened in B, which is a commit that is reachable from the tip of master (by looking at D's parent).

  • Git will, separately, compare the contents of the snapshot for commit A with that for the snapshot in E. This is, in effect, what happend in feature-A since commit A. Comparing A vs E includes whatever happened in C, since the snapshot in E includes whatever happened in C (well, minus any reversions back to what was in A).

Now that Git has the two sets of changes, Git will combine them, and apply the resulting bigger change to the snapshot in commit A. This gives it the contents needed to produce new snapshot/commit G:

...--A--B----F   <-- master
      \  \
       \  D---G
        \    /
         C--E

If you do this with git checkout feature-B && git merge feature-A, new commit G is added to the feature-B branch:

...--A--B----F   <-- master
      \  \
       \  D---G   <-- feature-B (HEAD)
        \    /
         C--E   <-- feature-A

If you do this with git checkout feature-A && git merge feature-B, new commit G is added to the feature-A branch instead, so we might draw it more like this:

...--A--B----F   <-- master
      \  \
       \  D   <-- feature-B
        \  `--_
         C--E--G   <-- feature-A

Either way, though, G is a merge commit, and therefore cannot be rebased, unlike commits D or C--E.

Using git merge --squash

It is, however, now possible to run:

git checkout master && git merge --squash <something>

The <something> here is anything that produces the hash ID of new commit G. You could type in the raw hash ID; or, if you did the merge so that commit G has been added to feature-B, you could use the name feature-B.

The git checkout master step attaches HEAD to master, and of course checks out the tip commit of master, i.e., commit F. So assuming G is on feature-B, this looks like:

...--A--B----F   <-- master (HEAD)
      \  \
       \  D---G   <-- feature-B
        \    /
         C--E   <-- feature-A

The git merge --squash step does the verb part of git merge, i.e., the work that git merge would do: it finds the merge base, just as before, and then runs two git diff --find-renames commands, just as before.

The merge base of commits F and G is commit B. This is because the merge base is, roughly speaking, the first commit that's reachable from both branch tips. From F, we step back once to B. From G, we step back once to D and once to E, then a second time from both D and E to both B and C. We've now reached a common point—commit B—so B is the merge base.

So, as before, Git runs git diff --find-renames B F and git diff --find-renames B G, to see what has happened in the two branches. Git then combines those changes together, applies them to the contents of snapshot B, and readies a new snapshot H.

Here git merge --squash departs from regular git merge in two ways:

  1. It skips the commit, as if you had added the --no-commit option, or as if there had been a merge conflict.
  2. When you do make the commit yourself manually, it records only one parent hash ID in the new commit.

So when you run git commit now, you get:

...--A--B----F--H   <-- master (HEAD)
      \  \
       \  D---G   <-- feature-B
        \    /
         C--E   <-- feature-A

The contents of commit H, i.e., the snapshot, is the same as if you had done a regular merge; but a regular merge would record the result as:

...--A--B----F--H   <-- master (HEAD)
      \  \     /
       \  D---G   <-- feature-B
        \    /
         C--E   <-- feature-A

Since the squash merge did not record the extra parent, Git will not know, later, that H already has all the work from G. (With a real merge, Git would know that, because the merge base would be commit G.)

What this means for the person doing the squash-merge is that it now becomes counterproductive to do any further development on either feature-A or feature-B, because they will be more difficult to merge into master later, because of the missing parent linkage. It's therefore probably appropriate to delete both names feature-A and feature-B at this point:

...--A--B----F--H   <-- master (HEAD)
      \  \
       \  D---G   [abandoned]
        \    /
         C--E   [abandoned]

As with rebasing, the extra commits C--E and D--G will stick around a short while (perhaps shorter than with rebase) even once the names are gone, but eventually they do go away. But remember, if anyone else has names for those original commits, saved away in some other repository, those commits—and those names—will remain forever in their repositories, unless you convince them to delete them. If they base their own work on those commits, they may have trouble, later, combining their work with your squashed commit H.

torek
  • 448,244
  • 59
  • 642
  • 775
  • so, it is not possible to squash? @torek – Sanjay Dutt Dec 25 '17 at 04:47
  • Is there a tl;dr for this? – evolutionxbox Dec 25 '17 at 20:14
  • @evolutionxbox: basically, you need to use `git merge --squash`, but unless you understand the consequences, you should never use `git merge --squash`. – torek Dec 25 '17 at 22:22
  • I’ve learnt not to be scared of git. As long as commits are reachable most things aren’t scary. – evolutionxbox Dec 25 '17 at 23:16
  • @evolutionxbox: sure: the consequences of `git merge --squash` here are not a problem for you and your Git, they're more potentially a problem for *other people* you're sharing your repository with via various clones. Everyone needs to know: this line of development has been squashed and is therefore moribund; you should get off it ASAP. – torek Dec 25 '17 at 23:42
  • what if the commits are from github ? – bUff23 Jan 16 '20 at 07:45
  • @bUff23: It doesn't really matter who made the original commits. What matters is the final *commit graph*, in *each Git repository*. If you do a squash-merge of someone else's commit(s) that you got from the someone-else, and expect them to abandon their commit(s) in favor of yours, you must let them know to abandon their commit(s) in favor of yours. Figure out who "they" are and how to let them know and make sure they agree that this is how you will all work together. Otherwise, by squashing, you have already stopped working together. – torek Jan 16 '20 at 16:55