#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 2009-2010 Martin Jelinek. All Rights Reserved.
#
# -----------------------------------------------------------------------
#
# program for updating viewvc database
# and sending e-mail changelists
#
# -----------------------------------------------------------------------
#

#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, they will remain None.
#

LIBRARY_DIR = None
CONF_PATHNAME = None

# Adjust sys.path to include our library directory
import sys
import os

if LIBRARY_DIR:
  sys.path.insert(0, LIBRARY_DIR)
else:
  sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))

#########################################################################
  
import os
import string
import re
import datetime

import svn.core
import svn.repos
import svn.fs
import svn.delta

import cvsdb
import viewvc
import vclib
import vclib.ccvs
import mailing




class CvsDbAdmin:
    def UpdateFile(self, db, repository, path, update, quiet_level):
        try:
            if update:
                commit_list = cvsdb.GetUnrecordedCommitList(repository, path, db)
            else:
                commit_list = cvsdb.GetCommitListFromRCSFile(repository, path)
        except cvsdb.error, e:
            print '[ERROR] %s' % (e)
            return
    
        file = string.join(path, "/")
        printing = 0
        if update:
            if quiet_level < 1 or (quiet_level < 2 and len(commit_list)):
                printing = 1
                print '[%s [%d new commits]]' % (file, len(commit_list)),
        else:
            if quiet_level < 2:
                printing = 1
                print '[%s [%d commits]]' % (file, len(commit_list)),
    
        ## add the commits into the database
        for commit in commit_list:
            db.AddCommit(commit)
            if printing:
                sys.stdout.write('.')
            sys.stdout.flush()
        if printing:
            print
        return commit_list
    
    
    def RecurseUpdate(self, db, repository, directory, update, quiet_level):
        new_commits = []
        for entry in repository.listdir(directory, None, {}):
            path = directory + [entry.name]
    
            if entry.errors:
                continue
    
            if entry.kind is vclib.DIR:
                commits = self.RecurseUpdate(db, repository, path, update, quiet_level)
                if (commits):
                    new_commits.extend(commits)
                continue
    
            if entry.kind is vclib.FILE:
                commits = self.UpdateFile(db, repository, path, update, quiet_level)
                if (len(commits)):
                    new_commits.extend(commits)
                
        return new_commits


class SvnDbAdmin:
    class SvnRepo:
        """Class used to manage a connection to a SVN repository."""
        def __init__(self, path, name):
            self.path = path
            self.repo = svn.repos.svn_repos_open(path)
            self.fs = svn.repos.svn_repos_fs(self.repo)
            self.rev_max = svn.fs.youngest_rev(self.fs)
            self.name = name
        def __getitem__(self, rev):
            if rev is None:
                rev = self.rev_max
            elif rev < 0:
                rev = rev + self.rev_max + 1
            assert 0 <= rev <= self.rev_max
            rev = SvnDbAdmin.SvnRev(self, rev)
            return rev
        def rootname(self):
            return self.name
        def roottype(self):
            return vclib.SVN
    
    class SvnRev:
        """Class used to hold information about a particular revision of
        the repository."""
        
        _re_diff_change_command = re.compile('(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?')
        
        def _get_diff_counts(self, diff_fp):
            """Calculate the plus/minus counts by parsing the output of a
            normal diff.  The reasons for choosing Normal diff format are:
              - the output is short, so should be quicker to parse.
              - only the change commands need be parsed to calculate the counts.
              - All file data is prefixed, so won't be mistaken for a change
                command.
            This code is based on the description of the format found in the
            GNU diff manual."""
        
            plus, minus = 0, 0
            line = diff_fp.readline()
            while line:
                match = re.match(self._re_diff_change_command, line)
                if match:
                    # size of first range
                    if match.group(2):
                        count1 = int(match.group(2)) - int(match.group(1)) + 1
                    else:
                        count1 = 1
                    cmd = match.group(3)
                    # size of second range
                    if match.group(5):
                        count2 = int(match.group(5)) - int(match.group(4)) + 1
                    else:
                        count2 = 1
        
                    if cmd == 'a':
                        # LaR - insert after line L of file1 range R of file2
                        plus = plus + count2
                    elif cmd == 'c':
                        # FcT - replace range F of file1 with range T of file2
                        minus = minus + count1
                        plus = plus + count2
                    elif cmd == 'd':
                        # RdL - remove range R of file1, which would have been
                        #       at line L of file2
                        minus = minus + count1
                line = diff_fp.readline()
            return plus, minus
        
        def __init__(self, repo, rev):
            self.repo = repo
            self.rev = rev
            self.rev_roots = {} # cache of revision roots
    
            # revision properties ...
            revprops = svn.fs.revision_proplist(repo.fs, rev)
            self.author = str(revprops.get(svn.core.SVN_PROP_REVISION_AUTHOR,''))
            self.date = str(revprops.get(svn.core.SVN_PROP_REVISION_DATE, ''))
            self.log = str(revprops.get(svn.core.SVN_PROP_REVISION_LOG, ''))
    
            # convert the date string to seconds since epoch ...
            try:
                self.date = svn.core.svn_time_from_cstring(self.date) / 1000000
            except:
                self.date = None
    
            # get a root for the current revisions
            fsroot = self._get_root_for_rev(rev)
            
            # find changes in the revision
            editor = svn.repos.RevisionChangeCollector(repo.fs, rev)
            e_ptr, e_baton = svn.delta.make_editor(editor)
            svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton)
    
            self.changes = []
            for path, change in editor.changes.items():
                # skip non-file changes
                if change.item_kind != svn.core.svn_node_file:
                    continue
    
                # deal with the change types we handle
                base_root = None
                if change.base_path:
                    base_root = self._get_root_for_rev(change.base_rev)
                    
                if not change.path:
                    action = 'remove'
                elif change.added:
                    action = 'add'
                else:
                    action = 'change'

                diffobj = svn.fs.FileDiff(base_root and base_root or None,
                                          base_root and change.base_path or None,
                                          change.path and fsroot or None,
                                          change.path and change.path or None)
                diff_fp = diffobj.get_pipe()
                plus, minus = self._get_diff_counts(diff_fp)
                self.changes.append((path, action, plus, minus))

        def _get_root_for_rev(self, rev):
            """Fetch a revision root from a cache of such, or a fresh root
            (which is then cached for later use."""
            if not self.rev_roots.has_key(rev):
                self.rev_roots[rev] = svn.fs.revision_root(self.repo.fs, rev)
            return self.rev_roots[rev]


    def handle_revision(self, db, command, repo, rev, verbose, force=0):
        """Adds a particular revision of the repository to the checkin database."""
        revision = repo[rev]
        committed = 0
    
        if verbose: print "Building commit info for revision %d..." % (rev),
    
        if not revision.changes:
            if verbose: print "skipped (no changes)."
            return
    
        for (path, action, plus, minus) in revision.changes:
            directory, file = os.path.split(path)
            commit = cvsdb.CreateCommit()
            commit.SetRepository(repo.path)
            commit.SetDirectory(directory)
            commit.SetFile(file)
            commit.SetRevision(str(rev))
            commit.SetAuthor(revision.author)
            commit.SetDescription(revision.log)
            commit.SetTime(revision.date)
            commit.SetPlusCount(plus)
            commit.SetMinusCount(minus)
            commit.SetBranch(None)
    
            if action == 'add':
                commit.SetTypeAdd()
            elif action == 'remove':
                commit.SetTypeRemove()
            elif action == 'change':
                commit.SetTypeChange()
    
            if command == 'update':
                result = db.CheckCommit(commit)
                if result and not force:
                    continue # already recorded
    
            # commit to database
            db.AddCommit(commit)
            committed = 1
    
        if verbose:
            if committed:
                print "done."
            else:
                print "skipped (already recorded)."
        
        if committed:
            return revision
    
    def main(self, command, repository, revs=[], verbose=0, force=0):
        new_revisions = []
        for rev in range(repository.rev_max+1):
            revision = self.handle_revision(db, command, repository, rev, verbose)
            if (revision):
                new_revisions.append(revision)
        return new_revisions


if __name__ == '__main__':
    args = sys.argv
    
    cfg = viewvc.load_config(CONF_PATHNAME)
    viewvc.expand_root_parents(cfg)
    db = cvsdb.ConnectDatabase(cfg)
    
    if (len(args) >= 2):
      repositories = db.GetRepositoryByPath(cvsdb.CleanRepository(args[1]))
    else:
      repositories = db.GetAllRepositories()
    
    for key in cfg.general.cvs_roots:
      cfg.general.cvs_roots[key] = cvsdb.CleanRepository(cfg.general.cvs_roots[key])
    for key in cfg.general.svn_roots:
      cfg.general.svn_roots[key] = cvsdb.CleanRepository(cfg.general.svn_roots[key])
    
    if (not repositories):
        print "No repository found."
    else:
        print "Updating repositories:"
        for repos_tuple in repositories:
          if repos_tuple[1]:
            repos = cvsdb.CleanRepository(repos_tuple[1])
            repos_id = repos_tuple[0]
            repos_name = None
            mailing_on = repos_tuple[2]
            
            roottype = None
            if (repos in cfg.general.cvs_roots.values()):
              roottype = 'cvs'
              for key in cfg.general.cvs_roots:
                if cfg.general.cvs_roots[key] == repos:
                  repos_name = key
            elif (repos in cfg.general.svn_roots.values()):
              roottype = 'svn'
              for key in cfg.general.svn_roots:
                if cfg.general.svn_roots[key] == repos:
                  repos_name = key
            
            if roottype:
              db.LockTablesWrite(["commits", "repositories", "dirs", "files", "branches",
                                  "descs", "people", "tags", "mailing", "users", "filetypes"])
              
              if roottype == 'cvs':
                #try:
                  repository = vclib.ccvs.CVSRepository(repos_name, repos, None,
                                                        cfg.utilities, 0)
                  
                  print "CVS repository: " + repos_name + "(id " + str(repos_id) + ")"
                  cvs_admin = CvsDbAdmin()
                  new_commits = cvs_admin.RecurseUpdate(db, repository, [], 1, 0)
                  
                  if ((mailing_on) and (new_commits)):
                    mailing.Mail_changes(cfg, db, repos_id, repository, new_commits)
                  
                #except:
                  #print 'chybka'
              elif roottype == 'svn':
                #try:
                  print "SVN repository: " + repos_name + "(id " + str(repos_id) + ")"
                  svn_admin = SvnDbAdmin()
                  repository = svn_admin.SvnRepo(repos, repos_name)
                  new_revisions = svn_admin.main('update', repository, None, 1)
                  
                  if ((mailing_on) and (new_revisions)):
                    mailing.Mail_changes(cfg, db, repos_id, repository, new_revisions)
                #except:
                  #print 'chybka'
                
              db.UnlockTables()