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 )