svnrepository-design

Subversion Repository Layout


Most subversion tools create a default repository layout with /trunk, /branches and /tags. The documentation also recommends not using separate repositories for each project, so that code can be more easily shared.

Following that advice has led to me having a repository with the following layout:

/trunk
      /Project1
      /Project2
/branches
         /Project1
         /Project2
/tags
     /Project1
     /Project2

and so on, you get the idea. Over time, I've found this structure a bit clumsy and it occurred to me that there's an alternative interpretation of the recommendations, such as:

/Project1
         /trunk
         /branches
         /tags
/Project2
         /trunk
         /branches
         /tags       

So, which layout do people use, and why? Or - is there another way to do things that I've completely missed?


Solution

  • I decided to bite the bullet and restructure my repository. I wrote a small program to assist (below). The steps I followed were:

    1. Make a backup copy of the original repository.
    2. svn checkout the entire repository. This took a long time and a lot of disk space.
    3. Run the program from below on the working copy from the previous step.
    4. Examine the modified working copy and tidy up any left over issues (eg. svn delete the obsolete trunk, tags and branches folders)
    5. svn commit back to the repository.

    This whole process took time, but I decided to take this approach because modifying a working copy is a lot safer than hacking up a live repository and I had the options to simply throw away the working copy if it all went wrong, to fix any issue in the working copy and commit the entire restructure as a single revision.

    Here's the C# code I used to do the moving. Requires SharpSvn library.

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.IO;
    using System.Linq;
    using System.Text;
    using SharpSvn;
    
    /**
     * 
     * Program operation:
     * 1. Parse command line to determine path to working copy root
     * 2. Enumerate folders in the /trunk 
     * 3. Restructure each project folder in /trunk
     * 
     * 
     * Restructure a Project:
     * 1. Get the project name (folder name in /trunk/{Project})
     * 2. SVN Move /trunk/{Project} to /{Project}/trunk
     * 3. Reparent Project, branches
     * 4. Reparent Project, tags
     * 
     * Reparent(project, folder)
     * If /{folder}/{Project} exists
     *   SVN Move /{folder}/{Project} to /{Project}/{Folder}
     * else
     *   Create folder /{Project}/{Folder}
     *   SVN Add /{Project}/{Folder}
     * 
     **/
    
    namespace TiGra.SvnRestructure
    {
        /// <summary>
        /// Restructures a Subversion repository from
        ///     /trunk|branches|tags/Project
        /// to
        ///     /Project/trunk|branches|tags
        /// </summary>
        internal class Program
        {
            private static string WorkingCopy;
            private static string SvnUri;
            private static string Branches;
            private static string Tags;
            private static string Trunk;
    
            private static SvnClient svn;
            private static List<string> Projects;
    
            private static void Main(string[] args)
            {
                ProcessCommandLine(args);
                CreateSvnClient();
                EnumerateProjectsInTrunk();
                RestructureProjects();
                Console.ReadLine();
            }
    
            private static void RestructureProjects()
            {
                foreach (var project in Projects)
                {
                    RestructureSingleProject(project);
                }
            }
    
            private static void RestructureSingleProject(string projectPath)
            {
                var projectName = Path.GetFileName(projectPath);
                var projectNewRoot = Path.Combine(WorkingCopy, projectName);
                bool hasBranches = Directory.Exists(Path.Combine(Branches, projectName));
                bool hasTags = Directory.Exists(Path.Combine(Tags, projectName));
                Reparent(Path.Combine(Trunk, projectName), Path.Combine(projectNewRoot, "trunk"));
                if (hasBranches)
                    Reparent(Path.Combine(Branches, projectName), Path.Combine(projectNewRoot, "branches"));
                if (hasTags)
                    Reparent(Path.Combine(Tags, projectName), Path.Combine(projectNewRoot, "tags"));
            }
    
            private static void Reparent(string oldPath, string newPath)
            {
                Console.WriteLine(string.Format("Moving {0} --> {1}", oldPath, newPath));
                svn.Move(oldPath, newPath, new SvnMoveArgs(){CreateParents = true});
            }
    
            private static void EnumerateProjectsInTrunk()
            {
                var list = EnumerateFolders("trunk");
                Projects = list;
            }
    
            /// <summary>
            /// Enumerates the folders in the specified subdirectory.
            /// </summary>
            /// <param name="trunk">The trunk.</param>
            private static List<string> EnumerateFolders(string root)
            {
                var fullPath = Path.Combine(WorkingCopy, root);
                var folders = Directory.GetDirectories(fullPath, "*.*", SearchOption.TopDirectoryOnly).ToList();
                folders.RemoveAll(s => s.EndsWith(".svn")); // Remove special metadata folders.
                return folders;
            }
    
            private static void CreateSvnClient()
            {
                svn = new SharpSvn.SvnClient();
            }
    
            /// <summary>
            /// Processes the command line. There should be exactly one argument,
            /// which is the path to the working copy.
            /// </summary>
            private static void ProcessCommandLine(string[] args)
            {
                if (args.Length != 1)
                    throw new ArgumentException("There must be exactly one argument");
                var path = args[0];
                if (!Directory.Exists(path))
                    throw new ArgumentException("The specified working copy root could not be found.");
                WorkingCopy = path;
                Branches = Path.Combine(WorkingCopy, "branches");
                Tags = Path.Combine(WorkingCopy, "tags");
                Trunk = Path.Combine(WorkingCopy, "trunk");
            }
        }
    }