Flavius
Flavius

Reputation: 13816

throw away a child of a merge commit, keep the other, replay all other commits

Abstract

The more I dug into the problem, the more I've got a cleaner summary of the problem: How to get the parent of a vertex in a DAG which actually is a tree within my application domain, using git infrastructure?

Have a nice read. Any ideas are welcome (git-specific).

Question

I have a repository whose history has been built by a script written by me by means of semi-automatic commands:

enter image description here

So I am able to put automatically any kind of information into the history as needed, for example those tags colored in yellow, as the commands are executed.

A key requirement of all this is that, when a merge conflict occures (and they must occur, because the script's very aim is to make the user aware of merge conflicts, and get in touch with the other users in order to find solutions to the problem - there is a problem if a merge conflict occures, and I want the script to stimulate communication among the users).

Now, with that in mind, let's suppose I want to revert a merge commit. This works great for non-conflicted merges, like install-def in this image. I just have to use git revert -m 1 install-def (for example).

The procedure becomes more complicated when I need to revert install-abc, because I want to revert precisely those commits, install-abc (the merge commit) and it's "grey" child from remotes/hacklet/abc/master.

In other words, to get a linear history of only profile/myfirstprofile and remotes/hacklet/def/master.

What is the easiest and the safest method to achieve this in a semi-automated script, supposing that I can annotate pretty much any commit with any tag? Preferably without having to do anything related to the DAG (or for that matter, recursion), since the script is written in bash.

The "revert" does an "uninstall". git revert -m 1 ... works for reverting a commit in the situation of install-def, but for install-abc it gives:

error: could not revert 18cff49... using hacklet/abc/master in profile
myfirstprofile hint: after resolving the conflicts, mark the corrected
paths hint: with 'git add <paths>' or 'git rm <paths>' hint: and
commit the result with 'git commit'

Note: the profile branch can contain many such install- commits, I want to get rid of only one of them. Neither "Installs", nor "profiles" can be nested.

Note: I know about "module" and "subtree". Please don't bother about mentioning them, the thing I'm doing here is specific to the application domain (git is part of the application), this is not about using git in the development cycle. I've analysed these two features thoroughly, they simply do not fit in the application domain of my script.


Addendum 1

I've hacked around to include this information, which makes it easier I think:

enter image description here

If you think this is getting overly complicated, it's not, it's basically the same pattern over and over again:

enter image description here

Here, only start-profile, install-abc and install-foo have had no conflicts. But the structure of the tree does look "the same".

And a last one, with all conflicts (except the first, install-abc), because I like the colourful output of gitk:

enter image description here

Yet another one, but with no conflicts at all:

enter image description here

Note: There may be individual commits which are not tagged in the profile/myfirstprofile branch. I such cases, those commits should be kept. Really nothing else should be "uninstalled", but the one commits belonging to install-THING.


Addendum 2

Though I've figured out that the corresponding before-install- can be found with git describe --tags $(git rev-list -1 install-abc). However:

Given the tag install-def, how to find the child commit - in the last image this is before-install-ghi?

Considering the last Note (untagged commits in-between), how to find these "child and parent" commits appropiate for sticking together, without the desired install-THING which I want to "uninstall"?

PS: I really understand what the word directed in directed acyclic graph means, but there must be some way specific to git, using git infrastructure and concepts, I smell it.

Note: I would like to refrain from having to iterate the tree every time I want to "uninstall" something.

Upvotes: 1

Views: 381

Answers (2)

Christopher
Christopher

Reputation: 44244

I find this explanation convoluted because it doesn't hedge to common git terminology, but as far as I can tell, you're asking one of three questions (pretty sure it's #3):

  1. "How can I reset to an earlier branched commit?"
  2. "How can I revert to a merge commit's parent without resetting a branch or checking out an earlier commit in history?"
  3. "How can I revert a merge commit when later downstream commits are based on the merge parent I'm reverting?"

For this explanation, consider this graph:

G
|
F
|\
D E
|/ 
C
|
B
|
A

First, some terms:

Commits 'D' and 'E' are "merge parents" of commit 'F', which is a merge commit. The graphs you see almost always have two parents, a "left" and a "right" in your terminology, a "first" and a "second" in git terminology, but there's no restriction on the number of parents a merge might have. Technically, so called "octopus merges" are possible, which could be three or more parents. They're just rarely used in practice.

Merge parents are canonically described using the ^ syntax in git. Please use this syntax because it'll make it easier to understand your question. The first parent is ^1, the second parent is ^2. In other words:

F^1 is equivalent to D
F^2 is equivalent to E

The most confusing aspect of your question is understanding what state you're trying to reach. So let's address the three possible ones:


"How can I reset to an earlier branched commit?"

Suppose you're trying to reach commit 'D' and don't care about changes introduced by 'E', 'F', or 'G'. Using Let_Me_Be's answer:

git reset --hard D

The same would work if you wanted instead to reach 'E', but didn't care at all about retaining the changes introduced by 'D', 'F', or 'G'.


"How can I revert to a merge commit's parent without resetting a branch or checking out an earlier commit in history?"

Suppose instead you want something more complex: Suppose you wanted to revert 'D's changes without losing the changes in 'E'or 'G'. That is also a trivial operation, so long as neither 'F' nor 'G' were based upon 'D's changes. In that case:

git revert -m 2 'F'

The command syntax mean "Revert the merge commit 'F' and make the second parent the mainline." The resulting history (again assuming 'G' wasn't based upon 'D') will look as if 'D' were never merged, but of course the actual history will look like this:

H
|
G
|
F
|\
D E
|/ 
C
|
B
|
A

where 'H' is the revert commit. If you wanted to revert 'F' and use 'D' as the mainline, just change the specified parent number in the command: git revert -m 1 'F' (note the use of 1 for "first parent").


"How can I revert a merge commit when later downstream commits are based on the merge parent I'm reverting?"

Okay. This is the trickiest situation. If you're doing this often, git is not the solution for your your problem, regardless of its context. Git tries to remain totally agnostic about conflict resolution if it can. That's the whole point: If merge conflicts could be solved by the software, it'd do it. If they arise regularly in your workflow, your workflow is the problem, not your VCS.

If you want to avoid history revision (i.e. git rebase, which would be crazy to automate fully even if we could), the only way to solve your revert conflicts are to revert the subsequent commits based on 'D's code. In other words:

git revert G
git revert -m 1 F

You could then reapply 'G' if you wanted with 'git cherry-pick G' (the original 'G' commit hash), but odds are applying it wouldn't go smoothly. Your resulting history would look something like:

J - Reapplication of G
|
I - Revert of F
|
H - Revert of G
|
G
|
F
|\
D E
|/ 
C
|
B
|
A

If this answer best describes your situation, I would strongly encourage you to look elsewhere than git for your application. It is not designed to do what you want easily, because what you're doing would be a fairly broken version control workflow amongst human beings. I'm skeptical of your app's ability to catch all the edge cases. Getting it to make intelligent conflict resolutions, even if the conflicts themselves are fairly predictable, is not an easy problem. I could of course be wrong.

Upvotes: 1

Šimon T&#243;th
Šimon T&#243;th

Reputation: 36433

This is more of a guess, then an answer (because I'm still incredibly confused by the question).

But my guess is that you want to do this:

git reset --hard start_myfirstprofile
git merge remotes/hacklet/def/master

If you want to reset to a state before the merge.

git reset HEAD~1

on the correct branch should be fine.

Upvotes: 0

Related Questions