Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When subproject is a subtree merge, how do I revert a commit that came from the subtree remote?

Tags:

git

I have a testcase shell script. I want to downgrade a sub-project, but can't figure out how. Here's the story:

I created two repos, "subproject" and "superproject". I committed several versions of some files into subproject, giving them tags "v0", "v1", "v2"... etc. In superproject, I added subproject as a repo and merged it as a subtree:

git remote add subproject ../subproject
git fetch --tags subproject
git merge -s ours --no-commit v1
git read-tree --prefix=subproject/ -u v1
git commit -m "added subproject at v1"

Next I update the subproject to v3, and that goes well:

git merge -s subtree -X subtree=subproject v3

But then I realize I really want v2. I think what I need here is a rebase, but I'm not sure. Here's what I tried:

git checkout -b downgrade-subproject-to-v2
git rebase -s subtree -X subtree=subproject --onto v2 downgrade-subproject-to-v2

But I see the contents of the subproject get spoojed out all over my superproject's main directory!

So I did some reading and found some other ideas. This one did not work:

git revert --no-edit v3

It also spoojed files in my superproject, and complained about conflicts. This also did not work:

git merge -s subtree -X subtree=subproject v2

Obviously, v3 has already been merged, so merging v2 (an ancestor of v3) will have no effect.

The only thing I've found that results in a downgrade is this:

git diff v3 v2 --src-prefix=subproject/ --dst-prefix=subproject/ | patch -p0

But I don't like it, because it doesn't actually show proper ancestry in the commit DAG. It is just a brute-force rewriting of the tree. Also, it relies on the patch program to do "merging", which is not nearly as smart as git merging can do. If superproject made any changes to subproject before the downgrade, patch will not be as likely to resolve merge errors as git merge would be.

Is there a canonical way to revert a sub-project version using subtree merges? If not, is there at least something better than diff|patch?

like image 657
Dave Avatar asked Apr 28 '11 18:04

Dave


People also ask

How do you revert a commit which is merged?

You can use the Git reset command to undo a merge. Firstly, you need to check for the commit hash (or id) so you can use it to go back to the previous commit. To check for the hash, run git log or git reflog . git reflog is a better option because things are more readable with it.

Can you revert a merge request?

Revert a merge requestAfter a merge request is merged, you can revert all changes in the merge request. Prerequisites: You must have a role in the project that allows you to edit merge requests, and add code to the repository.

How do I revert a committed code?

Steps to revert a Git commitLocate the ID of the commit to revert with the git log or reflog command. Issue the git revert command and provide the commit ID of interest. Supply a meaningful Git commit message to describe why the revert was needed.


1 Answers

Ultimately git revert v3 in superproject fails because it does not account for the fact that v3 is “behind” a subtree merge (thus the recorded pathnames differ in N and v3).

Revert atop superproject history

With Git 1.7.5 you can use

git revert --strategy subtree -X subtree=subproject v3

in place of your git diff | patch; commit method. It will potentially make better use of the merge machinery. However, with either method, the new commit’s parent will be built on the history of superproject (which may not be what you really want).

Your subproject history looks like this:

     (v1) (v2) (v3)
     /    /    /
----o----o----o

Your superproject history looks this:

     (v1) (v2) (v3)
     /    /    /
----o----o----o
     \         \
  ----M---------N

Both methods create commit O with this history:

     (v1) (v2) (v3)
     /    /    /
----o----o----o
     \         \
  ----M---------N----O

Revert atop subproject history

You might be happier with a commit built directly on top of v3. You could subtree merge such a commit into the superproject history like you do with any new subproject commit.

  • In subproject: checkout v3, revert it (R), and tag the result (revert-v3)

    git checkout v3
    git revert HEAD
    git tag revert-v3
    git checkout -       # go back to wherever you originally were
    

    Resulting history:

         (v1) (v2) (v3) (revert-v3)
         /    /    /    /
    ----o----o----o----R
    
  • In superproject: fetch the result, and use subtree merge (P) to incorporate it

    git fetch --tags subproject
    git merge -s subtree -X subtree=subproject revert-v3
    

    Resulting history:

         (v1) (v2) (v3) (revert-v3)
         /    /    /    /
    ----o----o----o----R
         \         \    \
      ----M---------N----P
    

If you do not want to have revert-v3 in subproject itself (e.g. because v3 is okay in the context of subproject, but is just not suitable for use in superproject), then you can do the work entirely in superproject at the cost of churning your working tree (switching back and forth between “unrelated” commits will effectively delete and restore all your working tree files, so the mtimes, inodes, etc. will have changed):

  • In superproject: checkout v3, revert it, tag it, go back to previous checkout, subtree merge the new commit

    git status        # make sure to start with a clean index and working tree
    git checkout v3
    git revert HEAD
    git tag revert-v3
    git checkout -
    git merge -s subtree -X subtree=subproject revert-v3
    

The major difference is that the final revert-v3 tag is only present in superproject in this second variation. The resulting history has the same structure and content as the first variation.

If you can not abide churning the working tree of either subproject or superproject and you do not want revert-v3 in subproject, then you can use a temporary clone of subproject (in the clone: revert the commit, tag it; in the superproject: fetch the tag from the clone, and subtree merge it; then delete the temporary clone).


git subtree

You may want to investigate apenwarr’s git subtree command. Its support for subtree merges is a bit more streamlined (e.g. git subtree pull -P prefix repository refspec). Also, its git subtree split command may be of particular interest since it would let you transform the commit made from the result of your git diff new old | patch (or git revert --strategy) into a commit that appears to have been made directly atop the subtree’s history.

git subtree split --prefix=subproject/ --onto v11 would take the “revert on top of superproject” history:

     (v1) (v2) (v3)
     /    /    /
----o----o----o
     \         \
  ----M---------N----R

and extract a subproject-only history like this:

     (v1) (v2) (v3)
     /    /    /
----o----o----o-----R'

where everything before R' is the original subproject history and R' is a version of R with only the content from subproject/.

This ability to “extract after the fact” means you can just work on the content itself in superproject without having to worry about whether the commits that touch subproject/ might be candidates for sending “upstream” to the subproject repository.

1 You could leave off --onto v1 if you used git subtree add to initially add the subtree2. It puts special text in the commit messages it generates so that it can identify the “subtree” bits of the history.

2 Or you could “convert to git subtree” with something like

git rm -rf subproject &&
git commit -m 'converting to subproject/ to "git subtree"' &&
git subtree add --prefix=subproject most-recent-subproject-commit
like image 54
Chris Johnsen Avatar answered Sep 29 '22 12:09

Chris Johnsen