Reputation: 1080
I created a private git repository for a project for a while back, and developed a pattern of creating a maintenance branch for each major version release.
A few months down the line in the project, I now have a number of branches for old versions, which are no longer under any form of support. I've realised that this is a messy way to handle the problem. As a tidier alternative, I'd like to maintain a development branch and a release branch, with tags for the individual versions instead.
I'd ideally like to convert the current repository to this new format. I want to preserve as much as the commit history as possible in terms of the relationship between release commits and commits on master, but I want to convert all of the release branches into one single release branch.
The repository is only used by me, so I could paste the contents of a branch onto the end of another branch quite easily with git rebase --onto
-- I'm not concerned about preserving the exact commit hashes here.
What I am concerned about preserving is the merge history with the master branch. I'd like to reparent the root commit of each branch so that is based on both the tip of the previous branch, and the commit where it originally branched from master -- turn it into a merge commit.
I'd then like to preserve a number of other merges between master and that branch, which happened further up the chain.
I tried something along the lines of git rebase --onto branchB-divergence...branchA-tip branchB
(and a couple of variants, including trying -m and -i, while trawling through the docs)
The result was either accidentally duplicating a lot of the commits on master, and losing the merge history, or constantly getting merge conflicts which needed resolving.
Is what I'm trying to achieve even possible? What sort of procedure would I follow to reparent a series of commits like this?
Upvotes: 2
Views: 1547
Reputation: 1080
Okay, so based on Philip's answer, I did a little bit of research into grafts, replace, and filter-branch.
Because what I want to achieve is to rewrite history, pretending that I laid my repository out correctly in the first place, I decided that the way to go about this would be to graft the head of each branch to the tip of the previous branch, then run git filter-branch to make the change permanent.
Under the new workflow, when a change is completed and ready to go into release, it will be merged into the release branch. If you inspect the history, I want it to look like those merges took place and were resolved in favour of whatever the original branch head was.
If you look too carefully at the result, you'll notice that there's actually no merge commit -- each grafted "merge" leads directly into then next commit. This is not really as big an issue to me as having dozens of spare branches floating around, so I'm going to leave it unresolved.
Grafting
I created the file .git/info/grafts
. For this example there would be two entries in the grafts file - one for each new branch parent.
The first mistake I made was forgetting to include the original parent of the commit as well -- half my attachments to master disappeared, and it took me about half of the process to notice.
Second time around I got the contents of the graft file right
[commit] [parent] [parent]
hash2 p2 hash1
hash4 p3 hash3
Cleaning
I wound up with some untracked files which had been deleted in earlier commits floating around in my working directory. I'm not sure about the mechanics of how they wound up there, but I know that they're preserved in my commit history, so I just deleted the working dir copies and ran git reset --hard
for good measure.
branch-C is already pointing at the tip of the new release branch, so I'm just going to rename it:
% git branch -m branch-C release
The next step in the grafting process will be to make the grafts permanent. This will require using git filter-branch
to completely rewrite the commit tree
Rewriting history is okay for me right now, because I'm the only person who actually uses this repository. Funny story, the main reason I'm cleaning it up is because I'm planning to share the repo with others soon, and I don't want a headache on my hands trying to fix the layout after other people are relying on a stable history.
Because I'll be recreating all those commits, I don't want the old branch pointers to hang around, keeping my old, dead commit trees visible.
% git branch -d branch-A
Deleted branch branch-A (was hash1).
% git branch -d branch-B
Deleted branch branch-B (was hash3).
Due to the grafts, there are no hanging tips for git to complain about, so the deletes go smoothly.
I also sync the repository off-site, so my tracking branches are going to hold on to those dead trees as well. This is going to be resolved later on with a push --force
, but for now, I'm just going to drop my remote so that it doesn't confuse me when I'm double checking my changes locally.
% git remote remove origin
Rewriting history
Okay, it's time to do the unthinkable. My git repository, locally, has the correct branch layout, and there are no pointers to any commits other than the tips of the release and master branches.
I've checked out the release
branch.
With no arguments, git filter-branch
will walk down the tree and rewrite history to make all of my grafts permanent.
% git filter-branch
Rewrite (...) (73/73)
Ref 'refs/heads/release' was rewritten
As yet, I haven't created any of those shiny tags that I wanted in my original plan - that's because the tag pointers would have been pointing at defunct commit hashes.
Now that I've inspected the results and my repository looks the way I want it to, I'm going to clean up the grafts file and add those tags.
.git/info/grafts
is now grafting commits that I don't care about, so, in confidence, I can either remove the file altogether, or clear out the offending entries.
% rm .git/info/grafts
The commit tree still looks right, so I've gone through and added all the tags I want to various releases.
There's one last step to rewrite history -- that remote repository I like to sync to.
I'm re-adding my remote, which will make my commit tree look all tangled and sick for a while.
% git remote add origin (...)
At first I tried just doing a regular git push --force --all
, but that didn't actually delete the defunct branches. There's an option for that, it's called --prune
.
% git push --force --all
Counting objects: 161, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (61/61), done.
Writing objects: 100% (86/86), 9.06 KiB, done.
Total 86 (delta 48), reused 0 (delta 0)
To (...)
* [new branch] release -> release
% git push --force --all --prune
To (....)
- [deleted] branch-A
- [deleted] branch-B
- [deleted] branch-C
Everything's perfect, so I've also deleted the backup info that git filter-branch made for the release branch:
% rm .git/refs/original/refs/heads/release
Upvotes: 2
Reputation: 14061
Don't do it that way. Why not create an --orphan
branch for 'LifeSupport', which a simply a final resting place, and then merge each of the dead branches onto it (merge strategy --ours
) and then add a tag to each of the last life-support commits with their appropriate version number. That way you can delete the old branch refs and still be able to get back to their old versions.
Alternatively use grafts
or replace
(Are grafts deprecated?) to join the old branch series into the chain you indicated. These can then be frozen in via filter-branch
.
Upvotes: 1