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?
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
: checkoutv3
, 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 itgit 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
: checkoutv3
, revert it, tag it, go back to previous checkout, subtree merge the new commitgit 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 v1
1 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
Perhaps using git submodules with the help of git slave would help. Alternatively, just use one repository and keep changes to the subproject in a separate branch or set of branches.
Hope this helps.
精彩评论