Subversion代码库布局

34

大多数版本控制工具都使用 /trunk、/branches 和 /tags 创建默认的仓库布局。文档还建议不要为每个项目使用单独的仓库,这样代码可以更容易地共享。

遵循这一建议导致我有了一个如下所示的仓库布局:

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

等等,你懂的。随着时间的推移,我发现这种结构有点笨拙,而且我意识到建议还有另一种解释:

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

那么,人们使用哪种布局以及原因是什么?还是说,有我完全没有想到的其他方法来处理这些问题吗?


使用同一代码库来支持多个项目,有什么好处可以帮助你共享代码呢? - Tomislav Nakic-Alfirevic
你告诉我吧;-) 这就是Subversion手册中所说的。 - Tim Long
6个回答

34

我发现Subversion Repository Layout博客文章总结得很好:

(...) 社区已经采用了几种常见的布局,将其视为最佳实践的推荐。如果您的仓库对公众开放,遵循这些惯例可能会使用户更容易找到他们想要的内容。

有两种常用的布局:

trunk
branches
tags

第一种布局是最佳选项,适用于只包含一个项目或与彼此紧密相关的一组项目的存储库。这种布局很有用,因为可以使用一个命令轻松地对整个项目或一组项目进行分支或标记:

svn copy url://repos/trunk url://repos/tags/tagname -m "Create tagname"

这可能是最常用的存储库布局之一,许多开源项目都在使用,例如Subversion和Subclipse自身。这是大多数托管网站(如Tigris.org、SourceForge.net和Google Code)遵循的布局,因为这些网站上的每个项目都有自己的存储库。

下一个布局适用于包含无关或松散相关项目的存储库,是最佳选项。

ProjectA
   trunk
   branches
   tags
ProjectB
   trunk
   branches
   tags

在这种布局中,每个项目都有一个顶层文件夹,然后在其下创建trunk/branches/tags文件夹。实际上,这与第一种布局是相同的,只是不是将每个项目放在自己的存储库中,而是全部放在单个存储库中。Apache软件基金会使用此布局来管理其存储库,其中包含所有项目的信息。

使用此布局,每个项目都有自己的分支和标签,并且可以使用一个命令轻松为该项目中的文件创建它们,类似于先前显示的命令:

svn copy url://repos/ProjectA/trunk url://repos/ProjectA/tags/tagname -m "Create tagname"

在这种布局中,您不能轻松创建包含ProjectA和ProjectB文件的分支或标记。尽管您仍然可以这样做,但需要多个命令,并且您还必须决定是否为涉及多个项目的分支和标记创建一个特殊文件夹。如果您需要经常执行此操作,则可能要考虑使用第一种布局。

因此,简而言之:

  • 对于单个或多个相关项目,请使用第一种布局。
  • 对于不相关的项目,请使用第二种布局。

整篇文章都值得一读。


8
第二种布局是最好的选择。一个很好的原因是允许或拒绝开发人员使用其中一个项目。

5

我更喜欢第二个。使用第二个选项,如果两个项目之间的人员权限不同,则更容易实现。


1
同样地,如果政治或商业需要,使用svn dump将它们拆分到单独的仓库中会更容易。 - retracile

3
我更喜欢第二种方法,使用maven或ant/ivy从其他项目中获取构件(如果需要的话)。
我还更喜欢每个仓库只有一个项目,或者一小部分相关的仓库。
这简化了访问控制,因为在仓库级别上比仓库内路径级别更容易进行身份验证 - 特别是在针对LDAP进行身份验证时。
备份/恢复操作最初会有点复杂,因为您必须循环遍历所有仓库才能进行热备份,但是在不幸的情况下,您只需要恢复一个仓库-其他仓库无需脱机或丢失任何数据。随着项目的停止,可以简单地删除仓库,从而节省未来备份的空间。
当每个仓库只有一个项目(或一小部分相关项目)时,挂钩脚本变得更简单,您无需检查受影响的路径以有条件地在挂钩中采取行动。
正如retracile所指出的那样,如果您想要选择性地使用svndumpfilter导出,一个庞大的单一存储库可能会带来巨大的麻烦-导致它死亡的更改路径数量可能很高。
将来版本的svn升级存储库架构需要更多的工作-您必须执行n次而不是一次...但是它可以脚本化,您不需要立即协调所有人。
如果有人提交了密码,并且您必须将其删除,则可以在一个仓库中快速进行转储/过滤/重新加载,而不影响其他团队。
如果您选择这条路,请注意每个仓库都有一个不同的.conf文件,而不是一个巨大的.conf文件,这样更容易管理,并提供了某些时间戳将会过时的舒适感-如果出现问题,您可以更轻松地查找最近的更改。

1

请参考svnbook中的Repository Layout章节

There are some standard, recommended ways to organize the contents of a repository. Most people create a trunk directory to hold the “main line” of development, a branches directory to contain branch copies, and a tags directory to contain tag copies.

/
   trunk/
   branches/
   tags/

If a repository contains multiple projects, admins typically index their layout by project.

here's an example of such a layout:

/
   paint/
      trunk/
      branches/
      tags/
   calc/
      trunk/
      branches/
      tags/

Of course, you're free to ignore these common layouts. You can create any sort of variation, whatever works best for you or your team. Remember that whatever you choose, it's not a permanent commitment. You can reorganize your repository at any time. Because branches and tags are ordinary directories, the svn move command can move or rename them however you wish. Switching from one layout to another is just a matter of issuing a series of server-side moves; if you don't like the way things are organized in the repository, just juggle the directories around.

Remember, though, that while moving directories is easy to do, you need to be considerate of other users as well. Your juggling can disorient users with existing working copies. If a user has a working copy of a particular repository directory and your svn move subcommand removes the path from the latest revision, then when the user next runs svn update, she is told that her working copy represents a path that no longer exists. She is then forced to svn switch to the new location.


1

我决定下定决心重构我的代码库。我写了一个小程序来协助(如下)。我遵循的步骤是:

  1. 备份原始代码库。
  2. svn checkout 整个代码库。这需要很长时间和大量磁盘空间。
  3. 在上一步的工作副本上运行下面的程序。
  4. 检查修改后的工作副本并清理任何剩余的问题(例如,svn delete 废弃的 trunktagsbranches 文件夹)。
  5. svn commit 回到代码库。

整个过程需要时间,但我决定采取这种方法,因为修改工作副本比直接修改代码库更安全,如果出现问题,我可以选择丢弃工作副本或修复工作副本中的任何问题,并将整个重构作为单个版本提交。

以下是我用于移动的 C# 代码。需要 SharpSvn 库。

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");
        }
    }
}

我不知道为什么这个答案被投票下降 - 它是一个真正解决实际问题的解决方案,怎么会是坏事呢?当我遇到毫无头绪的随机负评时,我真的很烦恼。 - Tim Long
不是我。但是,你以这种方式失去了你的修订历史记录。我用C#编写了一个程序,其中包含重构例程,用于将旧存储库中的修订历史记录复制到新存储库中。然后,它逐步检出每个修订版本,从最低到最高开始。在每个修订版本的检出时,应用重构例程,然后触发提交检入到新存储库。通过这种策略,我可以在TFS和SVN之间传输,包括修订历史记录。通过从旧存储库检出并检入到新存储库的90K次迭代,需要60小时才能完成一次运行。 - Blessed Geek
从哲学上讲,有一种观点认为你实际上失去了你的修订历史,因为你重写了历史以隐藏你所做的更改。我的版本保留了真实的历史记录,包括仓库重组。我并不是说任何一种方法都是错误的,每种情况都有其自身的优点和考虑因素,但我的方法满足了我的要求。事实上,其他人可能会喜欢不同的结果,并不会使它成为一个糟糕的答案。 - Tim Long

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接