#! /usr/bin/python

# (Be in -*- python -*- mode.)
#
# ====================================================================
# Copyright (c) 2006 CollabNet.  All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.  The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals.  For exact contribution history, see the revision
# history and logs, available at http://cvs2svn.tigris.org/.
# ====================================================================

"""Strip the text content out of RCS-format files.

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This script irretrievably destroys any RCS files that it is applied to! !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

Usage: destroy_repository.py PATH ...

This script attempts to strip the file text, log messages, and author
names out of RCS files.  (This is useful to make test cases smaller
and to remove much of the proprietary information that is stored in a
repository.)  Note that this script does NOT obliterate other
information that might also be considered proprietary: file names,
commit dates, etc.  In fact, it's not guaranteed even to obliterate
all of the file text, or to do anything else for that matter.

Each PATH that is a *,v file will be stripped.

Each PATH that is a directory will be traversed and all of its *,v
files stripped.

Other PATHs will be ignored.

The *,v files must be writable by the user running the script.
Typically CVS repositories are read-only, so you might have to run
something like

    $ chmod -R ug+w my/repo/path

before running this script.

Most cvs2svn behavior is completely independent of the text contained
in an RCS file.  (The text is not even looked at until OutputPass.)

The idea is to use this script when preparing test cases for problems
that you experience with cvs2svn.  Instead of sending us your whole
CVS repository, you should:

1. Make a copy of the original repository

2. Run this script on the copy (NEVER ON THE ORIGINAL!!!)

3. Verify that the problem still exists when you use cvs2svn to
   convert the 'destroyed' copy

4. Send us the 'destroyed' copy along with the exact cvs2svn version
   that you used, the exact command line that you used to start the
   conversion, and the options file if you used one.

Please also consider using shrink_test_case.py to localize the problem
even further.

"""

from __future__ import generators

import sys
import os
import shutil
import re
import traceback

sys.path.insert(0, os.path.dirname(os.path.dirname(sys.argv[0])))

from cvs2svn_lib.key_generator import KeyGenerator
import cvs2svn_rcsparse
from rcs_file_filter import WriteRCSFileSink
from rcs_file_filter import FilterSink


tmpdir = 'destroy_repository-tmp'

file_key_generator = KeyGenerator(1)

def get_tmp_filename():
    return os.path.join(tmpdir, 'f%07d.tmp' % file_key_generator.gen_id())


class Substituter:
    def __init__(self, template):
        self.template = template
        self.key_generator = KeyGenerator(1)

        # A map from old values to new ones.
        self.substitutions = {}

    def get_substitution(self, s):
        r = self.substitutions.get(s)
        if r == None:
            r = self.template % self.key_generator.gen_id()
            self.substitutions[s] = r
        return r


class LogSubstituter(Substituter):
    # If a log messages matches any of these regular expressions, it
    # is passed through untouched.
    untouchable_log_res = [
        re.compile(r'^Initial revision\n$'),
        re.compile(r'^file .+ was initially added on branch .+\.\n$'),
        re.compile(r'^\*\*\* empty log message \*\*\*\n$'),
        re.compile(r'^initial checkin$'),
        ]

    def __init__(self):
        Substituter.__init__(self, 'log %d')

    def _is_untouchable(self, log):
        for untouchable_log_re in self.untouchable_log_res:
            if untouchable_log_re.search(log):
                return True
        return False

    def get_substitution(self, log):
        if self._is_untouchable(log):
            return log
        else:
            return Substituter.get_substitution(self, log)


class DestroyerFilterSink(FilterSink):
    def __init__(self, author_substituter, log_substituter, sink):
        FilterSink.__init__(self, sink)

        self.author_substituter = author_substituter
        self.log_substituter = log_substituter

    def define_revision(
        self, revision, timestamp, author, state, branches, next
        ):
        FilterSink.define_revision(
            self, revision, timestamp,
            self.author_substituter.get_substitution(author),
            state, branches, next
            )

    def set_description(self, description):
        FilterSink.set_description(self, '')

    def set_revision_info(self, revision, log, text):
        FilterSink.set_revision_info(
            self, revision, self.log_substituter.get_substitution(log), ''
            )


class FileDestroyer:
    def __init__(self):
        self.log_substituter = LogSubstituter()
        self.author_substituter = Substituter('author%d')

    def destroy_file(self, filename):
        tmp_filename = get_tmp_filename()
        f = open(tmp_filename, 'wb')
        cvs2svn_rcsparse.parse(
            open(filename, 'rb'),
            DestroyerFilterSink(
                self.author_substituter,
                self.log_substituter,
                WriteRCSFileSink(f),
                )
            )
        f.close()

        # Replace the original file with the new one:
        os.remove(filename)
        shutil.move(tmp_filename, filename)

    def visit(self, dirname, names):
        for name in names:
            path = os.path.join(dirname, name)
            if os.path.isfile(path) and path.endswith(',v'):
                sys.stderr.write('Destroying %s...' % path)
                self.destroy_file(path)
                sys.stderr.write('done.\n')
            elif os.path.isdir(path):
                # Subdirectories are traversed automatically
                pass
            else:
                sys.stderr.write('File %s is being ignored.\n' % path)

    def destroy_dir(self, path):
        os.path.walk(path, FileDestroyer.visit, self)


if __name__ == '__main__':
    if not os.path.isdir(tmpdir):
        os.makedirs(tmpdir)

    file_destroyer = FileDestroyer()

    for path in sys.argv[1:]:
        if os.path.isfile(path) and path.endswith(',v'):
            file_destroyer.destroy_file(path)
        elif os.path.isdir(path):
            file_destroyer.destroy_dir(path)
        else:
            sys.stderr.write('File %s is being ignored.\n' % path)


