1#!/usr/bin/env python 2# 3# ==================================================================== 4# Licensed to the Apache Software Foundation (ASF) under one 5# or more contributor license agreements. See the NOTICE file 6# distributed with this work for additional information 7# regarding copyright ownership. The ASF licenses this file 8# to you under the Apache License, Version 2.0 (the 9# "License"); you may not use this file except in compliance 10# with the License. You may obtain a copy of the License at 11# 12# http://www.apache.org/licenses/LICENSE-2.0 13# 14# Unless required by applicable law or agreed to in writing, 15# software distributed under the License is distributed on an 16# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17# KIND, either express or implied. See the License for the 18# specific language governing permissions and limitations 19# under the License. 20# ==================================================================== 21 22"""\ 23__SCRIPTNAME__: checkout utility for sparse Subversion working copies 24 25Usage: 1. __SCRIPTNAME__ checkout VIEWSPEC-FILE TARGET-DIR 26 2. __SCRIPTNAME__ examine VIEWSPEC-FILE 27 3. __SCRIPTNAME__ help 28 4. __SCRIPTNAME__ help-format 29 30VIEWSPEC-FILE is the path of a file whose contents describe a 31Subversion sparse checkouts layout, or '-' if that description should 32be read from stdin. TARGET-DIR is the working copy directory created 33by this script as it checks out the specified layout. 34 351. Parse VIEWSPEC-FILE and execute the necessary 'svn' command-line 36 operations to build out a working copy tree at TARGET-DIR. 37 382. Parse VIEWSPEC-FILE and dump out a human-readable representation of 39 the tree described in the specification. 40 413. Show this usage message. 42 434. Show information about the file format this program expects. 44 45""" 46 47FORMAT_HELP = """\ 48Viewspec File Format 49==================== 50 51The viewspec file format used by this tool is a collection of headers 52(using the typical one-per-line name:value syntax), followed by an 53empty line, followed by a set of one-per-line rules. 54 55The headers must contain at least the following: 56 57 Format - version of the viewspec format used throughout the file 58 Url - base URL applied to all rules; tree checkout location 59 60The following headers are optional: 61 62 Revision - version of the tree items to checkout 63 64Following the headers and blank line separator are the path rules. 65The rules are list of URLs -- relative to the base URL stated in the 66headers -- with optional annotations to specify the desired working 67copy depth of each item: 68 69 PATH/** - checkout PATH and all its children to infinite depth 70 PATH/* - checkout PATH and its immediate children 71 PATH/~ - checkout PATH and its file children 72 PATH - checkout PATH non-recursively 73 74By default, the top-level directory (associated with the base URL) is 75checked out with empty depth. You can override this using the special 76rules '**', '*', and '~' as appropriate. 77 78It is not necessary to explicitly list the parent directories of each 79path associated with a rule. If the parent directory of a given path 80is not "covered" by a previous rule, it will be checked out with empty 81depth. 82 83Examples 84======== 85 86Here's a sample viewspec file: 87 88 Format: 1 89 Url: http://svn.apache.org/repos/asf/subversion 90 Revision: 36366 91 92 trunk/** 93 branches/1.5.x/** 94 branches/1.6.x/** 95 README 96 branches/1.4.x/STATUS 97 branches/1.4.x/subversion/tests/cmdline/~ 98 99You may wish to version your viewspec files. If so, you can use this 100script in conjunction with 'svn cat' to fetch, parse, and act on a 101versioned viewspec file: 102 103 $ svn cat http://svn.example.com/specs/dev-spec.txt | 104 __SCRIPTNAME__ checkout - /path/to/target/directory 105 106""" 107 108######################################################################### 109### Possible future improvements that could be made: 110### 111### - support for excluded paths (PATH!) 112### - support for static revisions of individual paths (PATH@REV/**) 113### 114 115import sys 116import os 117import urllib 118 119DEPTH_EMPTY = 'empty' 120DEPTH_FILES = 'files' 121DEPTH_IMMEDIATES = 'immediates' 122DEPTH_INFINITY = 'infinity' 123 124os_system = None 125args = None 126 127class TreeNode: 128 """A representation of a single node in a Subversion sparse 129 checkout tree.""" 130 131 def __init__(self, name, depth): 132 self.name = name # the basename of this tree item 133 self.depth = depth # its depth (one of the DEPTH_* values) 134 self.children = {} # its children (basename -> TreeNode) 135 136 def add_child(self, child_node): 137 child_name = child_node.name 138 assert not self.children.has_key(child_node) 139 self.children[child_name] = child_node 140 141 def dump(self, recurse=False, indent=0): 142 sys.stderr.write(" " * indent) 143 sys.stderr.write("Path: %s (depth=%s)\n" % (self.name, self.depth)) 144 if recurse: 145 child_names = self.children.keys() 146 child_names.sort(svn_path_compare_paths) 147 for child_name in child_names: 148 self.children[child_name].dump(recurse, indent + 2) 149 150class SubversionViewspec: 151 """A representation of a Subversion sparse checkout specification.""" 152 153 def __init__(self, base_url, revision, tree): 154 self.base_url = base_url # base URL of the checkout 155 self.revision = revision # revision of the checkout (-1 == HEAD) 156 self.tree = tree # the top-most TreeNode item 157 158def svn_path_compare_paths(path1, path2): 159 """Compare PATH1 and PATH2 as paths, sorting depth-first-ily. 160 161 NOTE: Stolen unapologetically from Subversion's Python bindings 162 module svn.core.""" 163 164 path1_len = len(path1) 165 path2_len = len(path2) 166 min_len = min(path1_len, path2_len) 167 i = 0 168 169 # Are the paths exactly the same? 170 if path1 == path2: 171 return 0 172 173 # Skip past common prefix 174 while (i < min_len) and (path1[i] == path2[i]): 175 i = i + 1 176 177 # Children of paths are greater than their parents, but less than 178 # greater siblings of their parents 179 char1 = '\0' 180 char2 = '\0' 181 if (i < path1_len): 182 char1 = path1[i] 183 if (i < path2_len): 184 char2 = path2[i] 185 186 if (char1 == '/') and (i == path2_len): 187 return 1 188 if (char2 == '/') and (i == path1_len): 189 return -1 190 if (i < path1_len) and (char1 == '/'): 191 return -1 192 if (i < path2_len) and (char2 == '/'): 193 return 1 194 195 # Common prefix was skipped above, next character is compared to 196 # determine order 197 return cmp(char1, char2) 198 199def parse_viewspec_headers(viewspec_fp): 200 """Parse the headers from the viewspec file, return them as a 201 dictionary mapping header names to values.""" 202 203 headers = {} 204 while 1: 205 line = viewspec_fp.readline().strip() 206 if not line: 207 break 208 name, value = [x.strip() for x in line.split(':', 1)] 209 headers[name] = value 210 return headers 211 212def parse_viewspec(viewspec_fp): 213 """Parse the viewspec file, returning a SubversionViewspec object 214 that represents the specification.""" 215 216 headers = parse_viewspec_headers(viewspec_fp) 217 format = headers['Format'] 218 assert format == '1' 219 base_url = headers['Url'] 220 revision = int(headers.get('Revision', -1)) 221 root_depth = DEPTH_EMPTY 222 rules = {} 223 while 1: 224 line = viewspec_fp.readline() 225 if not line: 226 break 227 line = line.rstrip() 228 229 # These are special rules for the top-most dir; don't fall thru. 230 if line == '**': 231 root_depth = DEPTH_INFINITY 232 continue 233 elif line == '*': 234 root_depth = DEPTH_IMMEDIATES 235 continue 236 elif line == '~': 237 root_depth = DEPTH_FILES 238 continue 239 240 # These are the regular per-path rules. 241 elif line[-3:] == '/**': 242 depth = DEPTH_INFINITY 243 path = line[:-3] 244 elif line[-2:] == '/*': 245 depth = DEPTH_IMMEDIATES 246 path = line[:-2] 247 elif line[-2:] == '/~': 248 depth = DEPTH_FILES 249 path = line[:-2] 250 else: 251 depth = DEPTH_EMPTY 252 path = line 253 254 # Add our rule to the set thereof. 255 assert not rules.has_key(path) 256 rules[path] = depth 257 258 tree = TreeNode('', root_depth) 259 paths = rules.keys() 260 paths.sort(svn_path_compare_paths) 261 for path in paths: 262 depth = rules[path] 263 path_parts = filter(None, path.split('/')) 264 tree_ptr = tree 265 for part in path_parts[:-1]: 266 child_node = tree_ptr.children.get(part, None) 267 if not child_node: 268 child_node = TreeNode(part, DEPTH_EMPTY) 269 tree_ptr.add_child(child_node) 270 tree_ptr = child_node 271 tree_ptr.add_child(TreeNode(path_parts[-1], depth)) 272 return SubversionViewspec(base_url, revision, tree) 273 274def checkout_tree(base_url, revision, tree_node, target_dir, is_top=True): 275 """Checkout from BASE_URL, and into TARGET_DIR, the TREE_NODE 276 sparse checkout item. IS_TOP is set iff this node represents the 277 root of the checkout tree. REVISION is the revision to checkout, 278 or -1 if checking out HEAD.""" 279 280 depth = tree_node.depth 281 revision_str = '' 282 if revision != -1: 283 revision_str = "--revision=%d " % (revision) 284 if is_top: 285 os_system('svn checkout "%s" "%s" --depth=%s %s' 286 % (base_url, target_dir, depth, revision_str)) 287 else: 288 os_system('svn update "%s" --set-depth=%s %s' 289 % (target_dir, depth, revision_str)) 290 child_names = tree_node.children.keys() 291 child_names.sort(svn_path_compare_paths) 292 for child_name in child_names: 293 checkout_tree(base_url + '/' + child_name, 294 revision, 295 tree_node.children[child_name], 296 os.path.join(target_dir, urllib.unquote(child_name)), 297 False) 298 299def checkout_spec(viewspec, target_dir): 300 """Checkout the view specification VIEWSPEC into TARGET_DIR.""" 301 302 checkout_tree(viewspec.base_url, 303 viewspec.revision, 304 viewspec.tree, 305 target_dir) 306 307def usage_and_exit(errmsg=None): 308 stream = errmsg and sys.stderr or sys.stdout 309 msg = __doc__.replace("__SCRIPTNAME__", os.path.basename(args[0])) 310 stream.write(msg) 311 if errmsg: 312 stream.write("ERROR: %s\n" % (errmsg)) 313 return 1 314 return 0 315 316def main(os_sys, args_in): 317 global os_system 318 global args 319 os_system = os_sys 320 args = args_in 321 322 argc = len(args) 323 if argc < 2: 324 return usage_and_exit('Not enough arguments.') 325 subcommand = args[1] 326 if subcommand == 'help': 327 return usage_and_exit() 328 elif subcommand == 'help-format': 329 msg = FORMAT_HELP.replace("__SCRIPTNAME__", 330 os.path.basename(args[0])) 331 sys.stdout.write(msg) 332 return 1 333 elif subcommand == 'examine': 334 if argc < 3: 335 return usage_and_exit('No viewspec file specified.') 336 fp = (args[2] == '-') and sys.stdin or open(args[2], 'r') 337 viewspec = parse_viewspec(fp) 338 sys.stdout.write("Url: %s\n" % (viewspec.base_url)) 339 revision = viewspec.revision 340 if revision != -1: 341 sys.stdout.write("Revision: %s\n" % (revision)) 342 else: 343 sys.stdout.write("Revision: HEAD\n") 344 sys.stdout.write("\n") 345 viewspec.tree.dump(True) 346 elif subcommand == 'checkout': 347 if argc < 3: 348 return usage_and_exit('No viewspec file specified.') 349 if argc < 4: 350 return usage_and_exit('No target directory specified.') 351 fp = (args[2] == '-') and sys.stdin or open(args[2], 'r') 352 checkout_spec(parse_viewspec(fp), args[3]) 353 else: 354 return usage_and_exit('Unknown subcommand "%s".' % (subcommand)) 355 356if __name__ == "__main__": 357 if main(os.system, sys.argv): 358 sys.exit(1) 359