Subject: [PATCH] Allow multiple tag and branch directories in git-svnimport

From: Stefan Sperling <[email-masked]>
Date: Tue, 21 Aug 2007 19:08:58 +0200

With this patch, users can specify colon-seperated lists of
directories containing tags and branches, respectively.

This makes git-svnimport much more usable in the real world
because there are Subversion repositories that use a deeper
tag and branch hierarchy than the recommended hierarchy of

	trunk/
	tags/
	branches/

The patch assumes my memleak fix patch from
http://marc.info/?l=git&m=118554191513822&w=2
but applies cleanly to current vanilla git head as well.

I'm not subscribed to the list so please Cc me in replies, thanks.

diff --git a/Documentation/git-svnimport.txt b/Documentation/git-svnimport.txt
index e97d15e..2cfc407 100644
--- a/Documentation/git-svnimport.txt
+++ b/Documentation/git-svnimport.txt
@@ -12,11 +12,11 @@ SYNOPSIS
 [verse]
 'git-svnimport' [ -o <branch-for-HEAD> ] [ -h ] [ -v ] [ -d | -D ]
 		[ -C <GIT_repository> ] [ -i ] [ -u ] [-l limit_rev]
-		[ -b branch_subdir ] [ -T trunk_subdir ] [ -t tag_subdir ]
-		[ -s start_chg ] [ -m ] [ -r ] [ -M regex ]
-		[ -I <ignorefile_name> ] [ -A <author_file> ]
-		[ -R <repack_each_revs>] [ -P <path_from_trunk> ]
-		<SVN_repository_URL> [ <path> ]
+		[ -b branch_subdir[:branch_subdir:...] ] [ -T trunk_subdir ]
+		[ -t tag_subdir[:tag_subdir:...] ] [ -s start_chg ] [ -m ]
+		[ -r ] [ -M regex ] [ -I <ignorefile_name> ]
+		[ -A <author_file> ] [ -R <repack_each_revs>]
+		[ -P <path_from_trunk> ] <SVN_repository_URL> [ <path> ]

 DESCRIPTION
@@ -26,11 +26,6 @@ repository, or incrementally import into an existing one.
 
 SVN access is done by the SVN::Perl module.
 
-git-svnimport assumes that SVN repositories are organized into one
-"trunk" directory where the main development happens, "branches/FOO"
-directories for branches, and "/tags/FOO" directories for tags.
-Other subdirectories are ignored.
-
 git-svnimport creates a file ".git/svn2git", which is required for
 incremental SVN imports.
 
@@ -53,11 +48,13 @@ When importing incrementally, you might need to edit the .git/svn2git file.
 -T <trunk_subdir>::
 	Name the SVN trunk. Default "trunk".
 
--t <tag_subdir>::
-	Name the SVN subdirectory for tags. Default "tags".
+-t <tag_subdir[:tag_subdir:..]>::
+	Colon-seperated list of names of subdirectories containing tags.
+	Default "tags".
 
--b <branch_subdir>::
-	Name the SVN subdirectory for branches. Default "branches".
+-b <branch_subdir[:branch_subdir:...]>::
+	Colon-seperated list of names of subdirectories containing branches.
+	Default "branches".
 
 -o <branch-for-HEAD>::
 	The 'trunk' branch from SVN is imported to the 'origin' branch within
diff --git a/git-svnimport.perl b/git-svnimport.perl
index fc9ea71..96f0926 100755
--- a/git-svnimport.perl
+++ b/git-svnimport.perl
@@ -38,9 +38,10 @@ sub usage() {
 	print STDERR <<END;
 Usage: ${\basename $0}     # fetch/update GIT from SVN
        [-o branch-for-HEAD] [-h] [-v] [-l max_rev] [-R repack_each_revs]
-       [-C GIT_repository] [-t tagname] [-T trunkname] [-b branchname]
-       [-d|-D] [-i] [-u] [-r] [-I ignorefilename] [-s start_chg]
-       [-m] [-M regex] [-A author_file] [-S] [-F] [-P project_name] [SVN_URL]
+       [-C GIT_repository] [-t tag_subdir[:tag_subdir:...]] [-T trunk_subdir]
+       [-b branch_subdir[:branch_subdir:...]] [-d|-D] [-i] [-u] [-r]
+       [-I ignorefilename] [-s start_chg] [-m] [-M regex]
+       [-A author_file] [-S] [-F] [-P project_name] [SVN_URL]
 END
 	exit(1);
 }
@@ -48,9 +49,9 @@ END
 getopts("A:b:C:dDFhiI:l:mM:o:rs:t:T:SP:R:uv") or usage();
 usage if $opt_h;
 
-my $tag_name = $opt_t || "tags";
-my $trunk_name = defined $opt_T ? $opt_T : "trunk";
-my $branch_name = $opt_b || "branches";
+my @tag_dirs = defined $opt_t ? split /:/,$opt_t : "tags";
+my $trunk_dir = defined $opt_T ? $opt_T : "trunk";
+my @branch_dirs = defined $opt_b ? split /:/,$opt_b : "branches";
 my $project_name = $opt_P || "";
 $project_name = "/" . $project_name if ($project_name);
 my $repack_after = $opt_R || 1000;
@@ -68,14 +69,18 @@ my $svn_dir = $ARGV[1];
 
 our @mergerx = ();
 if ($opt_m) {
-	my $branch_esc = quotemeta ($branch_name);
-	my $trunk_esc  = quotemeta ($trunk_name);
-	@mergerx =
-	(
-		qr!\b(?:merg(?:ed?|ing))\b.*?\b((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
-		qr!\b(?:from|of)\W+((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
-		qr!\b(?:from|of)\W+(?:the )?([\w\.\-]+)[-\s]branch\b!i
-	);
+	my @branch_escs;
+	foreach (@branch_dirs) { push @branch_escs, quotemeta ($_); }
+	my $trunk_esc  = quotemeta ($trunk_dir);
+
+	foreach my $branch_esc (@branch_escs) {
+		push (@mergerx,
+		(
+			qr!\b(?:merg(?:ed?|ing))\b.*?\b((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
+			qr!\b(?:from|of)\W+((?:(?<=$branch_esc/)[\w\.\-]+)|(?:$trunk_esc))\b!i,
+			qr!\b(?:from|of)\W+(?:the )?([\w\.\-]+)[-\s]branch\b!i
+		));
+	}
 }
 if ($opt_M) {
 	unshift (@mergerx, qr/$opt_M/);
@@ -452,29 +457,39 @@ sub project_path($$)
 
 sub split_path($$) {
 	my($rev,$path) = @_;
-	my $branch;
+	my $dir; # tag or branch dir
+
+	OUTER: foreach my $tag_dir (@tag_dirs) {
+		foreach my $branch_dir (@branch_dirs) {
+			if ($path =~ s#^/\Q$tag_dir\E/([^/]+)/?##) {
+				$dir = "/$1";
+			} elsif ($path =~ s#^/\Q$trunk_dir\E/?##) {
+				$dir = "/";
+			} elsif ($path =~ s#^/\Q$branch_dir\E/([^/]+)/?##) {
+				$dir = $1;
+			}
+			last OUTER if $dir;
+		}
+	}
 
-	if($path =~ s#^/\Q$tag_name\E/([^/]+)/?##) {
-		$branch = "/$1";
-	} elsif($path =~ s#^/\Q$trunk_name\E/?##) {
-		$branch = "/";
-	} elsif($path =~ s#^/\Q$branch_name\E/([^/]+)/?##) {
-		$branch = $1;
-	} else {
-		my %no_error = (
-			"/" => 1,
-			"/$tag_name" => 1,
-			"/$branch_name" => 1
-		);
-		print STDERR "$rev: Unrecognized path: $path\n" unless (defined $no_error{$path});
-		return ()
+	if (! $dir) {
+		my %no_error = ( "/" => 1, "/tags" => 1, "/branches" => 1 );
+		foreach (@tag_dirs) {
+			$no_error{"/$_"} = 1;
+		}
+		foreach (@branch_dirs) {
+			$no_error{"/$_"} = 1;
+		}
+		print STDERR "$rev: Could not determine tag or branch ",
+			"directory for path '$path'\n",
+			unless (defined $no_error{$path});
 	}
 	if ($path eq "") {
 		$path = "/";
 	} elsif ($project_name) {
 		$path = project_path($path, $project_name);
 	}
-	return ($branch,$path);
+	return ($dir,$path);
 }
 
 sub branch_rev($$) {
@@ -874,9 +889,32 @@ sub commit {
 
 		$dest =~ tr/_/\./ if $opt_u;
 
-		system('git-tag', $dest, $cid) == 0
-			or die "Cannot create tag $dest: $!\n";
-
+		# Since we support multiple directories that host tags we
+		# must support repositories that contain the same tag name
+		# in different tag directories for whatever reason,
+		# e.g. tags/jim/1.0-rc3 and tags/huck/1.0-rc3
+		#
+		# Also, because in Subversion a given tag directory could
+		# exist in revision A, be deleted in revision B and later
+		# recreated under the same name in revision C, we
+		# cannot guarantee that a tag is unique simply by
+		# looking at the tag's path.
+		#
+		# So if we fail to create the tag the first time, we try
+		# to create the tag with the same name except with the
+		# revision number of the current commit appended.
+		# Users can rename tags again later after the repository
+		# has been converted if they don't like this.
+		# It's still much better than failing to convert the
+		# repository alltogether.
+		if (system('git-tag', $dest, $cid) != 0) {
+			print STDERR "Could not create tag $dest, ",
+				"trying to create tag $dest-r$revision ",
+				"instead\n";
+			$dest = "$dest-r$revision";
+			system('git-tag', $dest, $cid) == 0
+				or die "Cannot create tag $dest: $? $!\n";
+		}
 		print "Created tag '$dest' on '$branch'\n" if $opt_v;
 	}
 	$branches{$branch}{"LAST"} = $cid;

-- 
Stefan Sperling <[email-masked]>                 Software Developer
elego Software Solutions GmbH                            HRB 77719
Gustav-Meyer-Allee 25, Gebaeude 12        Tel:  +49 30 23 45 86 96 
13355 Berlin                              Fax:  +49 30 23 45 86 95
http://www.elego.de                 Geschaeftsfuehrer: Olaf Wagner