4

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?

manugarciac
  • 185
  • 12
  • I'm not sure if I completely follow what you're saying, but I'll make this comment: Git has no concept of "which branch" a commit came from. In your second graphic, it could have been rendered with most of the x's on the first line and 2 x's on the second line; it represents the same commit graph this way. – Nayuki Jan 27 '15 at 19:05
  • Why not just revert master to the last commit before all the bad things happened? – Matthew Herbst Jan 27 '15 at 19:08
  • What does it mean that "all the history now seems to have been on a branch"? Where else would in be? An how can the master branch be in the wrong place? Does a branch have a physical location? – Sven Marnach Jan 27 '15 at 19:13
  • "all the history now seems to have been on a branch"... This is by definition. All commits in `git` will appear to be on at least one branch - those that don't get garbage collected. – twalberg Jan 27 '15 at 19:32

2 Answers2

2

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.

torek
  • 330,127
  • 43
  • 437
  • 552
  • How do I "erase" the commits to straigthen the line? I didn't get that part. – manugarciac Jan 27 '15 at 20:15
  • Technically you don't: you can't (well, not without some more tricky lower level git things) but you don't *have* to either. It's branch labels and other references (tags, stashes, etc) that make commits visible and reachable. Once you change any labels (like `master`) that made the old commits visible, they are—or might as well be, except for the recovery you can do for about a month—gone. So you just force branch `master` to point to the new target commit (using `git branch` or `git reset`) and you're done. – torek Jan 27 '15 at 20:30
  • If I make a "new master", when I make that the current master, will the old commits on the master stop being visible? – manugarciac Jan 27 '15 at 20:48
  • Yes, unless you keep a name (like `oldmaster`) around to keep them in view. Or, if you have a tag that keeps them visible, etc. The way to remember this is that it's references that make commits visible/find-able. – torek Jan 27 '15 at 21:08
  • I have the master and the fixed "new-master" now. I'm trying to do the switch, like this: git branch -m master old-master; git branch -m new-master master; git push -f origin master; Is that the correct way? Because I'm getting "remote: GitLab: You don't have permission", and I'm doing this in a brand new project I created under my own namespace. – manugarciac Jan 28 '15 at 12:52
  • Never mind, the master was protected. I unprotected it to do this and then reprotected it. – manugarciac Jan 28 '15 at 13:15
  • It worked! Thanks a lot. I have a nice clean history on my master now. The only thing that bothers me is that I made the commits on the new-master with cherry-pick, which kept the author and commit message, but the commit date isn't the original one. I can live with that though. – manugarciac Jan 28 '15 at 15:39
0

A git branch is merely a label that points to an individual commit. A commit does not know which branch(es) is/are currently pointing to it; nor does it know the history of which branches have pointed to it previously. Thus, the only thing that really matters (and is nontrivial to change) is the commit history itself.

The simplest way to clear this up is probably the following: Find the newest commit that you think represents a sensible state of your codebase, and run the following commands (let's say that that commit's hash is 123abc):

git checkout -B master 123abc
git push -f origin master

This will make master point to 123abc both locally (on the machine of whoever runs these commands) and on the server. When the other developers run git fetch, their origin/master will move to 123abc, and they can check it out and move their own master there with git checkout -B master origin/master (I'm not entirely sure about the syntax for this command, though, and I don't have a git repository at hand.)

Warning: Unless you have a branch pointing to the commits that are newer than 123abc, those commits will seemingly disappear. If you want to look at their contents later in order to clean it up and re-commit it, you should start by creating branch(es) for those commits, e.g. git branch tempbranch 567def.

Aasmund Eldhuset
  • 35,093
  • 4
  • 61
  • 79