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 )