Hello

I've written a little design document to specify 'views'.

Attached is also a 'proof-of-concept' python script which should behave pretty much the same as svn checkout does, except that it supports views
(it's not very well tested since I'm stuck with svn 1.4 on linux and
had to debug it on WinDoS, but at least "--view srv-side -F views.conf"
should work fine).

Please give me your feedback :)

Thanks
Martin


[[[
Contents
========

   0. Goals
   1. Design
   2. User Interface
   3. Examples
   4. API Changes


0. Goals
========

   Many users have very large trees of which they only want to
   checkout certain parts.  In Subversion 1.5 sparse directories
   have been introduced.

   This proposal adds 'views' on top of sparse directories.  A view
   is a list of subdirs and files with depth information.  On checkout
   with '--view NAME' is checked out and those paths are updated to
   the specified depth.


1. Design
=========

   Views are defined in a simple text file.  Empty lines and lines
   starting with a '#' are ignored.  All other lines must start with
   'view' or with one of the accepted depth values, followed by at
   least one whitespace and an argument.

   'view' starts a new section describing the view, and the argument
   is the name of the view.  All lines following the view up to the
   next view or end of file belong to this view.

   Lines starting with a depth value have a path as argument.  This
   path is to be updated to the given depth.  Intermediate directories
   which are not specified for the view are updated to depth 'empty'.
   It is an error to specify subdirs of a dir which has depth
   'infinity.  The special path '.' is used to set the depth of the
   root directory.  The default depth for the root is 'empty'.

   Also a new property 'svn:views' is introduced.  It is set on trunk
   and its content is a relative path to the views configuration file.


2. User interface
=================

   Checkout accepts the new option '--view' whose argument is the
   name of the view to checkout.  '--view' and '--depth' are
   mutually exclusive since the depth of the root directory is
   specified in the view config file.

   Additionally checkout accepts '-F' (--file) to specify a local
   view config file.  It is an error to use '-F' without '--view'.


3. Examples
===========

   Initializing the views: Create the file "views.conf" containing
   three views, "dev", "docs" and "srv-side".

      cat > views.conf << EOF
      view      dev
      files     .
      infinity  build
      infinity  subversion

      view      docs
      empty     .
      infinity  doc
      infinity  notes
      infinity  www

      view      srv-side
      empty     .
      infinity  tools/server-side
      infinity  contrib/server-side
      EOF
      svn add views.conf
      svn ps svn:views views.conf .
      svn ci

   Checkout of the view "dev":

      svn co --view dev http://svn.apache.org/repos/asf/subversion/trunk


   Checkout of the view "dev" using a local config file:

      svn co -F ./views.conf --view dev \
         http://svn.apache.org/repos/asf/subversion/trunk



4. API Changes
==============

   Only the following public function is added, the parameters are
   mostly the same as for svn_client_checkout3:

      svn_error_t*
      svn_client_checkout_view(svn_revnum_t *result_rev,
                               const char *URL, const char *view_name,
                               const char *view_file, const char *path,
                               const svn_opt_revision_t *peg_revision,
                               const svn_opt_revision_t *revision,
                               svn_boolean_t ignore_externals,
                               svn_boolean_t allow_unver_obstructions,
                               svn_client_ctx_t *ctx, apr_pool_t *pool);

   view_name:
      Name of the view to checkout, must be non-null and non-empty.

   view_file:
      Null if the views have to be read from the repository.  If
      non-null it must be a path to a local views config file.

]]]
#!/usr/bin/env python

import optparse
import os.path
import subprocess
import sys

class SvnViews:

    def __init__( self ):
        # URL to checkout
        self.url = None
        # name of the view to checkout
        self.view = None
        # name of the views config file
        self.views_file = None
        # local working copy to create
        self.wc_path = None
        # revision number to checkout
        self.rev_nr = None

        # maps subdirs to depth values
        self.subdirs = {}
        # depth of the root dir
        self.root_depth = "empty"

        # valid depth names
        self.depths = ( 'empty', 'files', 'immediates', 'infinity' )

    def run( self ):
        if not self.parse_cmdline():
            print "parse_cmdline failed."
            return 1
        if not self.read_conf():
            print "read_conf failed."
            return 1
        if not self.checkout():
            print "checkout failed."
            return 1
        print "done."
        return 0

    def parse_cmdline( self ):
        usage = "usage: svnviews.py [options] URL [WC-path]"
        parser = optparse.OptionParser( usage=usage, version="0.1" )
        parser.add_option( "-r", "--revision",
                           action="store", dest="rev_nr", default=None,
                           type="int",
                           help="revision number" )
        parser.add_option( "--username",
                           action="store", dest="username", default=None,
                           type="string",
                           help="specify username" )
        parser.add_option( "--password",
                           action="store", dest="password", default=None,
                           type="string",
                           help="specify password" )
        parser.add_option( "--no-auth-cache", "",
                           action="store_true", dest="no_auth_cache",
                           default=False,
                           help="do not cache authentication tokens" )
        parser.add_option( "--ignore-externals",
                           action="store_true", dest="ignore_externals",
                           default=False,
                           help="ignore externals definitions" )
        parser.add_option( "--non-interactive",
                           action="store_true", dest="non_interactive",
                           default=False,
                           help="do no interactive prompting" )
        parser.add_option( "--config-dir",
                           action="store", dest="config_dir", default=None,
                           type="string",
                           help="specify user configuration directory" )
        parser.add_option( "--view",
                           action="store", dest="view", default=None,
                           type="string",
                           help="specify the view to checkout" )
        parser.add_option( "-F", "--file",
                           action="store", dest="file", default=None,
                           type="string",
                           help="specify views configuration file" )
        parser.add_option( "--depth",
                           action="store", dest="depth", default=None,
                           type="choice",
                           choices=[ "empty", "files",
                                     "immediates", "infinity" ],
                           help="limit operation by depth" )
        (options, args) = parser.parse_args( sys.argv[1:] )
        nArgs = len(args)
        if nArgs < 1:
            print "missing URL."
            return False
        if nArgs > 2:
            print "too many arguments."
            return False
        self.url = args[0].rstrip( "/" )
        if nArgs == 2:
            self.wc_path = args[1]
        else:
            # set wc_path to last component of the URL
            slash = self.url.rfind( "/" )
            if slash < 0:
                print "'%s' seems to not be an url." % self.url
                return False
            self.wc_path = self.url[slash+1:]
        if options.view:
            self.view = options.view
        else:
            # default for normal checkout is infinity
            self.root_depth = "infinity"
        if options.file:
            self.views_file = options.file
        if options.depth:
            if options.view:
                print "--view and --depth are mutually exclusive."
                return False
            self.root_depth = options.depth
        self.svn_cmd = [ "svn", "***" ]
        if options.username:
            self.svn_cmd.append( "--username" )
            self.svn_cmd.append( options.username )
        if options.password:
            self.svn_cmd.append( "--password" )
            self.svn_cmd.append( options.password )
        if options.ignore_externals:
            self.svn_cmd.append( "--ignore-externals" )
        if options.non_interactive:
            self.svn_cmd.append( "--non-interactive" )
        if options.config_dir:
            self.svn_cmd.append( "--config-dir" )
            self.svn_cmd.append( options.config_dir )
        if options.rev_nr:
            self.svn_cmd.append( "-r" )
            self.svn_cmd.append( options.rev_nr )
        return True

    def read_conf( self ):
        if self.view == None:
            # normal checkout, no view config to read
            return True
        if self.rev_nr == None:
            # get HEAD revision from repos
            cmd = self.svn_cmd[:]
            cmd[1] = "info"
            cmd.append( self.url )
            rc, out, err = self.run_cmd( cmd )
            if rc != 0:
                print "getting HEAD revision failed."
                return False
            for line in out.split( "\n" ):
                if line.startswith( "Revision: " ):
                    self.rev_nr = int( line.split()[1] )
                    self.svn_cmd.append( "-r" )
                    self.svn_cmd.append( str(self.rev_nr) )
                    break
        if self.views_file:
            return self.read_conf_from_file()
        else:
            return self.read_conf_from_repos()

    def read_conf_from_file( self ):
        data = None
        try:
            ifd = open( self.views_file, "rb" )
            data = ifd.read( 1024*1024 )
            ifd.close()
        except:
            print "Reading views from '%s' failed." % self.views_file
            return False
        return self.parse_conf_data( data )

    def read_conf_from_repos( self ):
        cmd = self.svn_cmd[:]
        cmd[1] = "pg"
        cmd.append( "svn:views" )
        cmd.append( self.url )
        rc, out, err = self.run_cmd( cmd )
        if rc != 0:
            print "Error reading property 'svn:views'."
            print out
            print err
            return False
        fileurl = self.url + "/" + out.strip()
        cmd = self.svn_cmd[:]
        cmd[1] = "cat"
        cmd.append( fileurl )
        rc, out, err = self.run_cmd( cmd )
        if rc != 0:
            print "Error reading property 'svn:views'."
            print out
            print err
            return False
        return self.parse_conf_data( out )

    def parse_conf_data( self, data ):
        current_view = ""
        for line in data.split( "\n" ):
            line = line.strip()
            if len(line) == 0:
                # empty line, ignore
                pass
            elif line[0] == "#":
                # comment line, ignore
                pass
            else:
                # 'data' line
                parts = line.split( None, 1 )
                if len(parts) != 2:
                    print "Invalid line '%s'." % line
                    return False
                if parts[0] == "view":
                    current_view = parts[1]
                elif parts[0] in self.depths:
                    if current_view == self.view:
                        if not self.add_subdir( parts[1], parts[0] ):
                            return False
                else:
                    print "Invalid type in line '%s'." % line
                    return False
        return True

    def add_subdir( self, path, depth ):
        if path == ".":
            if depth == "infinite":
                print "Root must not have depth 'infinite'." % path
                return False
            self.root_depth = depth
            return True
        # call recursive version
        return self.add_subdir_r( path, depth, None )

    def add_subdir_r( self, path, depth, subdir ):
        if path in self.subdirs:
            if not subdir:
                print "Path '%s' is already added." % path
                return False
            if self.subdirs[subdir] == "infinite":
                print "Parent dir '%s' of '%s' has depth infinite." % \
                        ( path, subdir )
                return False
            return True
        slash = path.rfind( "/" )
        if slash == 0 or slash == (len(path)-1):
            print "Invalid path '%s'." % path
            return False
        elif slash > 0:
            # try to add parent dir
            if not self.add_subdir_r( path[0:slash], "empty", path ):
                return False
        self.subdirs[path] = depth
        return True

    def checkout( self ):
        print "get", self.wc_path
        cmd = self.svn_cmd[:]
        cmd[1] = "co"
        cmd.append( "--depth" )
        cmd.append( self.root_depth )
        cmd.append( self.url )
        cmd.append( self.wc_path )
        rc, out, err = self.run_cmd( cmd, False )
        if rc != 0:
            print "Checkout failed."
            return False
        subdirlist = self.subdirs.keys()
        subdirlist.sort()
        for subdir in subdirlist:
            if os.path.sep == "/":
                target = os.path.join( self.wc_path, subdir )
            else:
                target = os.path.join( self.wc_path,
                        subdir.replace( "/", os.path.sep ) )
            print "get", target
            cmd = self.svn_cmd[:]
            cmd[1] = "up"
            cmd.append( "--set-depth" )
            cmd.append( self.subdirs[subdir] )
            cmd.append( target )
            rc, out, err = self.run_cmd( cmd, False )
            if rc != 0:
                print "Update of '%s' failed." % subdir
                return False
        return True

    def run_cmd( self, cmd, pipes=True ):
        try:
            PIPE=None
            if pipes:
                PIPE=subprocess.PIPE
            proc = subprocess.Popen( cmd, stdout=PIPE, stderr=PIPE )
            out, err = proc.communicate()
            return ( proc.returncode, out, err )
        except:
            return ( -1, "", "" )

if __name__ == "__main__":
    sv = SvnViews()
    rc = sv.run()
    sys.exit( rc )

Reply via email to