Thread (22 messages) flat view 22 messages, 8 authors, 2016-06-15

Re: Approaches to SVN to Git conversion

From: Stephen Bash <hidden>
Date: 2016-06-15 22:53:13

----- Original Message -----
From: "Andrew Sayers" <redacted>
Sent: Monday, March 5, 2012 6:27:32 PM
Subject: Re: Approaches to SVN to Git conversion
quoted
My current thinking (and this is very much open for discussion) is
that as long as the SVN properties are available (especially the
copyfrom information) Git has just as much information (if not more)
to reconstruct the SVN history as SVN does.  (And going through our
messy history I haven't found any counterpoint to this yet)
I agree that git can be taught a superset of the information in SVN,
but you'll need absolutely all SVN properties available...
I'm pretty sure Jonathan won't be happy with anything less ;)
I wrote my SVN exporter based on SVN dumps for three reasons - I
figured people switching from SVN would be more comfortable
customising a solution that only used technologies they understood, I
figured it might be useful to Mercurial or Bazaar some day if it was
DVCS-neutral, and I have to use SVN for my day job so I'm more
interested in getting a good migration story today than a great one
tomorrow.
The multiple systems argument is a good one.
quoted
  my %branch_spec = { '/trunk/projname' => 'master',
                      '/branches/*/projname' => '/refs/heads/*' };
  my %tag_spec = { '/tags/*/projname' => '/refs/tags/*' };

Now I know this simple mapping will fail as I get further in our
history -- in particular we have one branch that came from:

  svn cp $SVN_REPO/trunk/ $SVN_REPO/foo  # OOPS! not in branches!
  svn mv $SVN_REPO/foo $SVN_REPO/branches/foo

It's then up to the user to modify the branch
map to something that accounts for this behavior:

  my %branch_spec = { '/trunk/projname' => 'master',
                      '/branches/*/projname' => '/refs/heads/*',
                      '/foo' => '/refs/heads/foo' };
  my %tag_spec = { '/tags/*/projname' => '/refs/tags/*' };
I started with an approach like you describe, but as you say it winds
up in a mess of special cases.  A friend pointed me to Perl's catalyst
repository[2], which is a wonderful haven of every mad SVN thing ever
dreamt up.  That got me playing with more general heuristics, and
while writing this e-mail I think I've finally nailed it.  What do you
say to defining SVN branches like this:

A directory is a branch if...
1. it is not a subdirectory of an existing branch; and
2. either:
2a. it is in a list of branches specified by the user, or
2b. it is copied from a (subdirectory of a) branch
I think I started with a very similar set of rules...  Looking at my code now I'm having a hard time summarizing them (probably because they evolved with the code, so what started simple morphed into something pretty complicated).  I guess as long as the user has the option to say "no, don't treat this copy as a branch" (or equivalently the Git side of things has a way to say "ignore this branch") these rules would be okay.  But at that point we're back to a list of exceptions -- really we're arguing white-list vs black-list... I eventually chose to go the white-list route for our conversion after starting with black-list (a white-list that still required a few manual edits before manipulating the Git history).  So take that single data point for what it's worth.
quoted
quoted
Once the format is defined, git import is fairly straightforward.
Proof-of-concept code to follow, but it's really just a wrapper
around git-commit-tree, git-mktag etc.  I wrote this in Perl
thinking it would relate somehow to git-svn, but eventually
realised it didn't and that a few hundred calls to (plumbing)
processes per second isn't so good for performance.  The only
interesting part of the problem is how to tackle SVN tags.  I went
for an ambitious approach, making normal tags where possible and
downgrading them to lightweight tags when necessary.  This does
involve managing something that is effectively a branch in
refs/tags/, but what else is an SVN tag but a branch in the wrong
namespace?
I don't understand how "normal" and "lightweight" apply in this
situation? ... In the case of actual content changes in a tag's
life, I think it's up to the user to decide between three options:

  1) only retain the last SVN tag
  2) tag using the git-svn-style 'tagname@rev' for all but the last
  3) Do (2), but move older tags to some hidden namespace
     (refs/hidden/tags or the like)

... In the bidirectional case things get murky (maybe always tag
with tagname@rev and hope for tab completion?).
I didn't explain this particularly well, as it's based largely on the
vague desire to make update work some day.  Imagine the user does
this:

* git svn-pull # get tags/foo, a candidate for an annotated tag
... time passes ...
* git svn-pull # tags/foo has now been updated in another revision

If we create an annotated tag in step 1, what do we do in step 2?  You
can't make the tag object the parent of a new revision, so you need to
do something unpleasant.  The solution I proposed was to convert the
tag message to a commit message (i.e. pretend a lightweight tag had
been created all along), then add another commit on top of it and make
a lightweight tag from the new commit (i.e. treat it like a branch).
In retrospect that's far too much magic without user involvement - a
better solution would be to give the user this option along with the
ones you outlined, and let git-config remember their preference if
they want.
Okay, that's what I thought you meant (and what I classified as a bidirectional problem, but I guess it's not strictly a bidirectional problem, but a one-time migration does not have the problem).  If you want to continue to update Git from SVN there are two cases to consider:

  1) Each Git repository *only* talks to SVN
  2) The Git repository is cloned for further use 
     (So the chain is something like SVN->Git->Git)

In (1) your lightweight tag solution is probably okay (but I'm pretty sure creating/deleting annotated tags would behave the same way because no one else sees the Git tag object).  In (2) I think there would still be a tag conflict when the upstream Git repo replaces a lightweight tag and the downstream repo attempts to fetch it.  I don't know what the fetch/pull machinery does when there's a lightweight tag conflict (I'm guessing either bails out or keeps the local one?).  Case (2) motivates me to say always generate (annotated?) tags named tagname@rev so there can be no conflicts.  In that case the only difference I see is if we create an empty Git commit with the tag message plus a lightweight tag or tag the original commit with an annotated tag (I think it's fairly obvious I'm a fan of
  the latter).
[1] http://en.wikipedia.org/wiki/Full_employment_theorem
[2] http://dev.catalyst.perl.org/repos/bast/
Thanks,
Stephen
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help