I have the following situation in my GIT repository. Someone had forgotten to make a pull in the master before doing his changes, and then commited on his local master. After that, for some reason he merged the origin/master into his local master and then pushed that. The result was that the origin/master kinda "switched places" with what was his local master. Am I making any sense? Here is an example:
BEFORE THE PUSH
x----x-----x----x----x----x----x-----x----x (MASTER)
AFTER THE PUSH
---------------------------------------------x---x (MASTER)
| |
x----x-----x----x----x----x----x-----x----x-------
That kinda messed up the repository, as all the history now seems to have been on a branch.
After that, there were some new commits pushed to the new master, and then for a reason that isn't important right now, we decided we didn't want those, so we managed to drop the commits we didn't want, and at the same time restore de MASTER to its old place. Like this:
BEFORE
---------------------------------------------x---x---x---x---x (MASTER)
| |
x----x-----x----x----x----x----x-----x----x-------
AFTER
(2)
---------------------------------------------x---x---x---x---x--
| | |
x----x-----x----x----x----x----x-----x----x-----x----------------x (MASTER)
(1) (3)
As you can see, now that commit that was donde by the guy who forgot tu pull has been merged into what originally was the master. This was achieved like this:
git checkout <HASH OF COMMIT MARKED AS (1) >
git checkout -b refactor_master
git merge --no-ff <HASH OF COMMIT MARKED AS (2) >
git push origin refactor_master
git merge --strategy=ours mastergit checkout master
git merge refactor_master
git push origin master
That efectively made the changes incorporated by those commits dissapear from the master, and also turned the master to what it used to be. However, I now have a "branch" that should not have existed. In fact, the last commit, marked as (3), does not make any changes. It only "switches" the masters. Is there any way to make those commits dissapear?
It does make sense: what he did was to violate the "main line of development is first-parent" rule.
Note that there is nothing in git itself that can enforce this rule. It's not possible, for one simple reason: who defines which line is the "main line"? The only possible answer to that question is "you", where "you" means "whoever runs git to manipulate the commit-graph". So it's not really a git rule, it's a "people who use git" rule.
Whenever you run git merge
(or in this case "he" runs it), you choose your current branch as the main line of development, and whatever you are merging-in as the alternate line that is being merged-in. Thus, if you do this:
$ git checkout master
$ make-some-change; git add ...; git commit -m message
$ git fetch origin # and let's assume this brings in a new commit
$ git merge origin/master
you are telling git to keep your master as the main line, and merge in upstream changes as a branch line.
Note that the last two commands—git fetch
followed by git merge
—are what git pull
does by default. This, in turn, means that "main line is first-parent" is quite commonly violated and can't be depended on unless you are very strict / careful.
Is there any way to make those [merge] commits disappear?
Yes, but only by writing a new line of commits ("rewriting history").
Let me take your final graph (without worrying about how you got there) and make a few minor changes to the drawing for more compact representation:
------------------------A---M1--B--C--D
/ / \
o--o--o--o--o--o--o--o--o---x-------------M2 <-- master
Commits B
through D
are "on the wrong line" at this point because the first parent of merge commit M2
is x
, and its second parent is D
. Meanwhile commit A
is the first parent of M1
and x
is M1
's second parent.
If you really care a lot about the first-parent rule, you can make a new line of commits coming off commit x
:
------------------------A---M1--B--C--D
/ / \
o--o--o--o--o--o--o--o--o---x-------------M2 <-- master
\
A'--B'--C'--D' <-- new-master
Here A'
's first and only parent is commit x
, which was the tip commit of master
when things first "went wrong", as it were. Then B'
's first and only parent is A', and so on.
If, once you have this graph, you erase from your whiteboard commits A
through M2
and make master
point to commit D'
, you'll have this:
o--o--o--o--o--o--o--o--o---x
\
A'--B'--C'--D' <-- master
and now you can "straighten out" the link from x
to A'
and it looks like a nice linear history.
Here's the tricky part though: this is simply the graph you want. For each commit in the graph, git keeps a tree: a set of files to put in your work-directory when you git checkout
that commit. The tree you want for each commit A'
through D'
may not be exactly the same as the original trees on A
through D
.
It's pretty certain that the trees you want for B'
, C'
, and D'
will be the same as the ones you had for B
, C
, and D
respectively. However, the tree you want for new commit A'
is probably the one that is currently under merge M1
. This may be the same as the one under commit A
, but it might not be. It really depends on how A
compares with M1
.
There are a bunch of relatively tricky ways to build the new commits without a lot of manual work, but they are hard to describe in text. In addition, this kind of "history rewrite"—the part that happens when you forcibly make the old master
label point to new-master
's commit D'
—imposes pain on all your developers, who are making commits that have M2
as their parent commit. They must copy those commits to new commits with the new D'
as their parents.
It's up to you and them as to whether this pain is worth it.