#!/usr/bin/env python # -*-python-*- # # Copyright (C) 2004 James Henstridge # # By using this file, you agree to the terms and conditions set forth in # the LICENSE.html file which can be found at the top level of the ViewVC # distribution or at http://viewvc.org/license-1.html. # # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- # # administrative program for loading Subversion revision information # into the checkin database. It can be used to add a single revision # to the database, or rebuild/update all revisions. # # To add all the checkins from a Subversion repository to the checkin # database, run the following: # /path/to/svndbadmin rebuild /path/to/repo # # This script can also be called from the Subversion post-commit hook, # something like this: # REPOS="$1" # REV="$2" # /path/to/svndbadmin update "$REPOS" "$REV" # # If you allow changes to revision properties in your repository, you # might also want to set up something similar in the # post-revprop-change hook using "rebuild" instead of "update" to keep # the checkin database consistent with the repository. # # ----------------------------------------------------------------------- # ######################################################################### # # 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 svn.core import svn.repos import svn.fs import svn.delta import cvsdb import viewvc class SvnRepo: """Class used to manage a connection to a SVN repository.""" def __init__(self, path, pool): self.scratch_pool = svn.core.svn_pool_create(pool) self.path = path self.repo = svn.repos.svn_repos_open(path, pool) self.fs = svn.repos.svn_repos_fs(self.repo) # youngest revision of base of file system is highest revision number self.rev_max = svn.fs.youngest_rev(self.fs, pool) 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 = SvnRev(self, rev, self.scratch_pool) svn.core.svn_pool_clear(self.scratch_pool) return rev _re_diff_change_command = re.compile('(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?') def _get_diff_counts(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(_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 class SvnRev: """Class used to hold information about a particular revision of the repository.""" def __init__(self, repo, rev, pool): self.repo = repo self.rev = rev self.pool = pool self.rev_roots = {} # cache of revision roots subpool = svn.core.svn_pool_create(pool) # revision properties ... properties = svn.fs.revision_proplist(repo.fs, rev, pool) self.author = str(properties.get(svn.core.SVN_PROP_REVISION_AUTHOR,'')) self.date = str(properties.get(svn.core.SVN_PROP_REVISION_DATE, '')) self.log = str(properties.get(svn.core.SVN_PROP_REVISION_LOG, '')) # convert the date string to seconds since epoch ... self.date = svn.core.secs_from_timestr(self.date, pool) # 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, pool) e_ptr, e_baton = svn.delta.make_editor(editor, pool) svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton, pool) self.changes = [] for path, change in editor.changes.items(): # clear the iteration subpool svn.core.svn_pool_clear(subpool) # 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, subpool, []) diff_fp = diffobj.get_pipe() plus, minus = _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, self.pool) return self.rev_roots[rev] def handle_revision(db, command, repo, rev, verbose): """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: continue # already recorded # commit to database db.AddCommit(commit) committed = 1 if verbose: if committed: print "done." else: print "skipped (already recorded)." def main(pool, command, repository, rev=None, verbose=0): cfg = viewvc.load_config(CONF_PATHNAME) db = cvsdb.ConnectDatabase(cfg) repo = SvnRepo(repository, pool) if rev: handle_revision(db, command, repo, rev, verbose) else: for rev in range(repo.rev_max+1): handle_revision(db, command, repo, rev, verbose) def usage(): cmd = os.path.basename(sys.argv[0]) sys.stderr.write(""" Usage: 1. %s [-v] rebuild REPOSITORY [REVISION] 2. %s [-v] update REPOSITORY [REVISION] 1. Rebuild the commit database information for REPOSITORY across all revisions or, optionally, only for the specified REVISION. 2. Update the commit database information for REPOSITORY across all revisions or, optionally, only for the specified REVISION. This is just like rebuilding, except that no commit information will be stored for commits already present in the database. Use the -v flag to cause this script to give progress information as it works. """ % (cmd, cmd)) sys.exit(1) if __name__ == '__main__': verbose = 0 args = sys.argv try: index = args.index('-v') verbose = 1 del args[index] except ValueError: pass if len(args) < 3: usage() command = string.lower(args[1]) if command not in ('rebuild', 'update'): sys.stderr.write('ERROR: unknown command %s\n' % command) usage() repository = args[2] if not os.path.exists(repository): sys.stderr.write('ERROR: could not find repository %s\n' % repository) usage() if len(sys.argv) > 3: rev = sys.argv[3] try: rev = int(rev) except ValueError: sys.stderr.write('ERROR: revision "%s" is not numeric\n' % rev) usage() else: rev = None repository = cvsdb.CleanRepository(os.path.abspath(repository)) svn.core.run_app(main, command, repository, rev, verbose)