Subversionにaddされた同一ファイルを検出してリポジトリをダイエットする

背景

Subversionリポジトリを長く運用していると、同じファイルが何度も新規でAddされていることに気づくことがあります。プロジェクトによっては、バイナリをaddせざるを得なくなることも少なくなく、その場合、確実に重複したファイルがリポジトリ上に存在する状況になります。

実際、僕が運用している会社のリポジトリでは、xxxx.dllという全く同じファイルがあっちにもこっちにもaddされているという実情がありました。それぞれのプロジェクト担当者は、そのファイルが他のプロジェクトでも使われていることを気づいていないわけで、仕方ないわけではあります。

そこで、これらの同一ファイルを検出して、その間に嘘のSvn Copy履歴を追加するという処理をすれば、リポジトリの肥大化や非効率化への対策になり、また、変更のトラッキングに有利になるのではないかと考え、適当にコードを書いて見ました。

とは言っても、リポジトリに対して調節処理を行うのはほぼ不可能なので、ダンプファイルに対して処理を行います。

使い方

一気に処理を行うならば、

svnadmin create 新リポジトリパス
svnadmin dump リポジトリパス | svnhistforge | svnadmin load 新リポジトリパス

で良いでしょう。ちょっと怖いなという人は、

svnadmin dump リポジトリパス | svnhistforge | gzip > repo-dump.gz

でダンプを吐く方が良いでしょう(gzipで圧縮しています)。

これからリポジトリを復元するには、

svnadmin create 新リポジトリパス
gzip -cd repo-dump.gz | svnadmin load 新リポジトリパス

注意点

若干の注意が必要そうなのは、処理効率のために、ファイルの比較にはSHA1ハッシュ値とファイルサイズしか使っていないと言うことです。そのため、SHA1の衝突によってはおかしな履歴を追加してしまうことがあるかもしれません。

ソースコード

ライセンスは、修正済みBSDライセンスでお願いします。

コンパイルは、Visual C# 2005ならば、

csc /o svnhistforge.cs

Monoならば、

mgcs -optimize svnhistforge.cs

でいけるはずです。

// svnhistforge.cs - Subversion History Forge
// (C) 2007 T.Kawasaki

using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;

namespace SvnHistoryForge
{
  //
  class SHF_EntryPoint
  {
    //
    static void Main(string[] args)
    {
      ProcessSvnHistory(
        Console.OpenStandardInput(),
        Console.OpenStandardOutput());
    }

    //
    class FileEntry
    {
      public FileEntry(int rev, string path, int length)
      {
        m_rev = rev;
        m_path = path;
        m_length = length;
      }

      public int Revision { get { return m_rev; } }
      public string Path { get { return m_path; } }
      public int Length { get { return m_length; } }

      int m_rev;
      string m_path;
      int m_length;
    }

    //
    static void ProcessSvnHistory(Stream inputStream, Stream outputStream)
    {
      Dictionary<string, FileEntry> files = new Dictionary<string, FileEntry>();
      Dictionary<string, FileEntry> revLocal = null;
      int rev = 0;
      for (; ; )
      {
        Dictionary<string, string> hdr = LoadHeader(inputStream);
        if (hdr == null)
          break; // EOF
        if (hdr.Count == 0)
          continue;

        if (hdr.ContainsKey("Revision-number"))
        {
          rev = int.Parse(hdr["Revision-number"]);
          if (revLocal != null)
          {
            foreach (KeyValuePair<string, FileEntry> fe in revLocal)
            {
              files[fe.Key] = fe.Value;
            }
          }
          revLocal = new Dictionary<string, FileEntry>();
          Console.Error.WriteLine("Revision: {0}", rev);
        }

        if (!hdr.ContainsKey("Text-content-length") ||
          !hdr.ContainsKey("Node-action") ||
          hdr["Node-action"] != "add")
        {
          // pass-through
          OutputHeader(outputStream, hdr);
          OutputContentIfAny(outputStream, hdr, inputStream);
          continue;
        }

        string path = hdr["Node-path"];

        // load content
        byte[] propBuf = ReadContent(inputStream, hdr, "Prop-content-length");
        byte[] buf = ReadContent(inputStream, hdr, "Text-content-length");

        if (buf != null)
        {
          string hash = GetHashString(buf);
          revLocal[hash] = new FileEntry(rev, path, buf.Length);

          if (files.ContainsKey(hash))
          {
            FileEntry old = files[hash];
            if (old.Length == buf.Length)
            {
              hdr["Node-copyfrom-rev"] = old.Revision.ToString();
              hdr["Node-copyfrom-path"] = old.Path;
              hdr["Text-content-md5"] = "";
              hdr["Text-content-length"] = "";
              hdr["Content-length"] = (propBuf != null) ? propBuf.Length.ToString() : "";
              buf = null; // never emit the body
              Console.Error.WriteLine("  Copy: {0} ({1}) -> {2} ({3})",
                path, rev, old.Path, old.Revision.ToString());
            }
          }

        }

        OutputHeader(outputStream, hdr);
        if (propBuf != null)
          outputStream.Write(propBuf, 0, propBuf.Length);
        if (buf != null)
          outputStream.Write(buf, 0, buf.Length);

        outputStream.Write(new byte[2] { 0xA, 0xA }, 0, 2);
      }
    }

    //
    static string GetHashString(byte[] bin)
    {
      byte[] hash;
      using (SHA1 sha1 = SHA1.Create())
        hash = sha1.ComputeHash(bin);

      string chrs = "0123456789ABCDEF";
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < hash.Length; i++)
      {
        byte b = hash[i];
        sb.Append(chrs[b >> 4]);
        sb.Append(chrs[b & 15]);
      }
      return sb.ToString();
    }

    //
    static byte[] ReadContent(Stream inputStream, Dictionary<string, string> hdr, string lengthField)
    {
      if (!hdr.ContainsKey(lengthField))
        return null;
      int len = int.Parse(hdr[lengthField]);
      byte[] buf = new byte[len];
      int pos = 0;
      while (pos < len)
      {
        int ret = inputStream.Read(buf, pos, len - pos);
        if (pos < len && ret == 0)
          throw new ApplicationException("Cannot load content!");
        pos += ret;
      }
      return buf;
    }

    //
    static void OutputHeader(Stream outputStream, Dictionary<string, string> hdr)
    {
      foreach (KeyValuePair<string, string> kvp in hdr)
      {
        if(kvp.Value == "")
          continue;
        string hdrLine = kvp.Key + ": " + kvp.Value + "\xA";
        byte[] bin = Encoding.UTF8.GetBytes(hdrLine);
        outputStream.Write(bin, 0, bin.Length);
      }
      outputStream.WriteByte(0xA);
    }

    //
    static void OutputContentIfAny(Stream outputStream, Dictionary<string, string> hdr, Stream inputStream)
    {
      byte[] buf = ReadContent(inputStream, hdr, "Content-length");
      if (buf != null)
        outputStream.Write(buf, 0, buf.Length);

      if (hdr.ContainsKey("Node-path") && hdr.ContainsKey("Content-length"))
        outputStream.Write(new byte[2] { 0xA, 0xA }, 0, 2);
      else
        outputStream.WriteByte(0xA);
    }

    //
    static Dictionary<string, string> LoadHeader(Stream inputStream)
    {
      List<byte> hdrBin = new List<byte>(1024 * 1024);
      byte prev = 0;
      for (; ; )
      {
        int b = inputStream.ReadByte();
        if ((b == 0xA && prev == 0xA) || b < 0)
          break;
        hdrBin.Add((byte)b);
        prev = (byte)b;
      }

      if (hdrBin.Count == 0)
        return null;

      string hdrStr = Encoding.UTF8.GetString(hdrBin.ToArray());
      hdrBin = null; // we don't need any more

      string[] sep = { ": " };
      Dictionary<string, string> hdr = new Dictionary<string, string>();
      foreach (string kvs in hdrStr.Split('\xA'))
      {
        if (kvs == "")
          continue;
        string[] kv = kvs.Split(sep, 2, StringSplitOptions.None);
        hdr[kv[0]] = kv[1];
      }
      return hdr;
    }
  }
}