You cannot get what you want. You can get several things that may or may not be good enough. In particular, if you use a plain git merge
(with --no-ff
if/when required), Git records the commits. Git isn't really concerned with files, as it's about commits. Git isn't really very much concerned about branches either: it's still all about commits.
Illustration, with graph-drawings
Using your example, creating a new empty repository and stopping at this point:
$ git commit -m "First commit"
you now have a repository with exactly one commit in it. That one commit has some big ugly hash ID, but I'll just use the letter A
to stand in for it. The repository now has one named branch, master
, which holds the hash ID of this one commit A
, so we can draw it like this:
A <-- master (HEAD)
Now we run your second series of commands:
$ git checkout -b new-branch
At this point we have:
A <-- new-branch (HEAD), master
$ echo "blabla" >> ans
$ echo "blupp" > zwa
$ git add .
$ git commit -m "Commit on new-branch"
This creates new commit B
, dragging the name new-branch
forward:
A <-- master
\
B <-- new-branch (HEAD)
Now we use your third series of commands (I'll trim one down a bit):
$ git checkout -b another-branch master
This switches back to commit A
, creates a new branch name pointing to it, attaches HEAD
to the new branch, and leaves us with:
A <-- another-branch (HEAD), master
\
B <-- new-branch
$ echo "test" >> ans
$ echo "three" > dra
$ git add .
$ git commit -m "Commit on another-branch"
This creates third commit C
, dragging the name another-branch
forward to it:
C <-- another-branch (HEAD)
/
A <-- master
\
B <-- new-branch
Now we take on the hard part, the two merge
commands. We start with:
$ git checkout master
This extracts the contents of commit A
into the index/staging-area and your work-tree, and attaches the name HEAD
to the name master
:
C <-- another-branch
/
A <-- master (HEAD)
\
B <-- new-branch
$ git merge --squash new-branch
This does a merge operation that consists of diffing commit A
vs commit A
(which shows no differences of course) and then commit A
vs commit B
(which shows some differences). The changes discovered in these two diffs get combined—without any conflict, since one set of changes is "do nothing"—and Git stops short of making a new commit, so we need:
$ git commit -m "Squash new-branch"
which does make the new commit. I'll call this D
:
C <-- another-branch
/
A---D <-- master (HEAD)
\
B <-- new-branch
Note that commit D
has no backwards-looking connection to commit B
; it remembers only the hash ID of existing commit A
. Had you used:
git merge --no-ff new-branch
to make D
, we'd have a connecting line (should be an arrow but arrow fonts don't always work right on every browser) going down-and-right from D
to B
:
A---D
\ /
B
but we don't.
Next:
$ git merge --squash another-branch
This time the merge operation consists of diffing commit A
(the merge base) with commit D
, to see what we changed, then diffing commit A
vs commit C
(the tip commit of another-branch
), to see what they changed. The merge is actually conflicted—we both changed the file ans
, in lines that abut at the end of the file—so most forms of merge will stop here with a conflict. Hence you needed:
$ git mergetool
to resolve the conflict, though we could do that in shell with:
$ cat << END > ans
bla
blabla
test
END
$ git add ans
(you can substitute whatever you want for the merge result in the here-document section before the END
). The last step here is to commit the merge. Since it is a squash-merge, rather than a real merge, once again we do not have a backwards link to commit C
, even though we used commit C
to do the merge:
$ git commit -m "Squash another-branch"
This makes new commit/snapshot E
, so let's draw that:
C <-- another-branch
/
A---D--E <-- master (HEAD)
\
B <-- new-branch
Note that new commit E
has no connection back to C
. Using the name master
, Git starts at commit E
, walks back to D
, then walks back to A
. Commit A
has no parent—it's the very first commit we ever made, after all—so the action stops at this point. Commits B
and C
are not found in this process.
Why git branch -d
fails
If we don't use git mergetool
, we don't need git clean
to clean up its junk files, so I'll skip over that and proceed to:
$ git branch -d new-branch
> error: The branch 'new-branch' is not fully merged.
> If you are sure you want to delete it, run 'git branch -D new-branch'.
What this is telling you is that from commit E
—where you are now—there's no way to find commit B
. That's true; we will not find B
in a walk from E
back to the root. In fact, the name new-branch
is the only way we have to find commit B
. (Remember, B
stands in for some random-looking hash ID that we would never be able to guess.) If we do delete the name new-branch
, we will lose commit B
.
Since Git is all about commits, losing a commit would be bad. Git won't discard the name, and hence lose the commit, unless you force it.
The same goes for commit C
, find-able only through the name another-branch
. The git branch -d
command will refuse to delete it as commit C
is not an ancestor of current commit E
.
If we'd used regular git merge
—with --no-ff
the first time, since otherwise Git would have cheated with a fast-forward instead of a merge—we'd have, at this point, this graph:
C___ <-- another-branch
/ \
A---D--E <-- master (HEAD)
\ /
B <-- new-branch
Now each request, to delete the names new-branch
and another-branch
, would be "safe", because starting at commit E
, Git can walk back to commits D
and C
. From D
, Git can walk back to commits A
and B
. Commits B
and C
are therefore on branch master
, as well as being on branches new-branch
and another-branch
respectively. It's safe to delete the name new-branch
as commit B
is protected by being on master
. It's safe to delete the name another-branch
as commit C
is protected by being on master
.
Commits are history
Fundamentally, using git merge --squash
is a way to tell Git: I'm going to throw away some commits / history. If we have:
...--A--B--C--D--E <-- branch1 (HEAD)
\
F--G--H <-- branch2
and we run git merge --squash branch2
, we'll make a new commit on the current branch branch1
that is the result of combining the diff from C
to E
—what we did on branch1
—with the diff from C
to H
—what they did on branch2
. After successfully making this new commit:
...--A--B--C--D--E--FGH <-- branch1 (HEAD)
\
F--G--H <-- branch2
the only sensible thing to do with branch2
is to delete it. Git doesn't immediately delete it because we might have some other plans for some of its commits—for instance, maybe we want to cherry-pick G
or H
as a new commit in some other branch—but eventually we should kill it off. But our new FGH
combined commit doesn't remember the hash of commit H
, so deleting the name branch2
will lose commits F
, G
, and H
; so git branch
requires that we force this deletion.
Note that if there is some additional commit beyond H
, find-able through some other name:
...--A--B--C--D--E--FGH <-- branch1 (HEAD)
\
F--G--H <-- branch2
\
I--J <-- branch3
this additional name will keep commit J
alive, and J
will keep I
which keeps H
which keeps G
which keeps F
. Here, git branch -d branch2
will still fail from branch1
—commit H
is not an ancestor of commit FGH
—but will succeed when run from branch3
.
The precise definition of when a branch-name delete must be forced has evolved a bit over time. Git used to just use the current commit (HEAD
) and the branch-tip to decide if a deletion was safe. Now it also considers the branch's upstream setting, if the branch has one. If the branch tip commit is an ancestor of the branch's upstream's tip commit, but not of the current commit, Git now deletes the branch with a warning. The commits are safe—they're protected by the upstream name, at least for the moment—but Git isn't sure you really meant to do this, so it prints a warning including the hash ID that was stored in the branch name, to allow you to restore the branch name using that hash ID.