blob: cdcd4952d83373802b68f26253cd89a64ae6da7e [file] [log] [blame]
#!/usr/bin/env python
#
# ====================================================================
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ====================================================================
"""\
__SCRIPTNAME__: checkout utility for sparse Subversion working copies
Usage: 1. __SCRIPTNAME__ checkout VIEWSPEC-FILE TARGET-DIR
2. __SCRIPTNAME__ examine VIEWSPEC-FILE
3. __SCRIPTNAME__ help
4. __SCRIPTNAME__ help-format
VIEWSPEC-FILE is the path of a file whose contents describe a
Subversion sparse checkouts layout, or '-' if that description should
be read from stdin. TARGET-DIR is the working copy directory created
by this script as it checks out the specified layout.
1. Parse VIEWSPEC-FILE and execute the necessary 'svn' command-line
operations to build out a working copy tree at TARGET-DIR.
2. Parse VIEWSPEC-FILE and dump out a human-readable representation of
the tree described in the specification.
3. Show this usage message.
4. Show information about the file format this program expects.
"""
FORMAT_HELP = """\
Viewspec File Format
====================
The viewspec file format used by this tool is a collection of headers
(using the typical one-per-line name:value syntax), followed by an
empty line, followed by a set of one-per-line rules.
The headers must contain at least the following:
Format - version of the viewspec format used throughout the file
Url - base URL applied to all rules; tree checkout location
The following headers are optional:
Revision - version of the tree items to checkout
Following the headers and blank line separator are the path rules.
The rules are list of URLs -- relative to the base URL stated in the
headers -- with optional annotations to specify the desired working
copy depth of each item:
PATH/** - checkout PATH and all its children to infinite depth
PATH/* - checkout PATH and its immediate children
PATH/~ - checkout PATH and its file children
PATH - checkout PATH non-recursively
By default, the top-level directory (associated with the base URL) is
checked out with empty depth. You can override this using the special
rules '**', '*', and '~' as appropriate.
It is not necessary to explicitly list the parent directories of each
path associated with a rule. If the parent directory of a given path
is not "covered" by a previous rule, it will be checked out with empty
depth.
Examples
========
Here's a sample viewspec file:
Format: 1
Url: http://svn.apache.org/repos/asf/subversion
Revision: 36366
trunk/**
branches/1.5.x/**
branches/1.6.x/**
README
branches/1.4.x/STATUS
branches/1.4.x/subversion/tests/cmdline/~
You may wish to version your viewspec files. If so, you can use this
script in conjunction with 'svn cat' to fetch, parse, and act on a
versioned viewspec file:
$ svn cat http://svn.example.com/specs/dev-spec.txt |
__SCRIPTNAME__ checkout - /path/to/target/directory
"""
#########################################################################
### Possible future improvements that could be made:
###
### - support for excluded paths (PATH!)
### - support for static revisions of individual paths (PATH@REV/**)
###
import sys
import os
import urllib
DEPTH_EMPTY = 'empty'
DEPTH_FILES = 'files'
DEPTH_IMMEDIATES = 'immediates'
DEPTH_INFINITY = 'infinity'
class TreeNode:
"""A representation of a single node in a Subversion sparse
checkout tree."""
def __init__(self, name, depth):
self.name = name # the basename of this tree item
self.depth = depth # its depth (one of the DEPTH_* values)
self.children = {} # its children (basename -> TreeNode)
def add_child(self, child_node):
child_name = child_node.name
assert not self.children.has_key(child_node)
self.children[child_name] = child_node
def dump(self, recurse=False, indent=0):
sys.stderr.write(" " * indent)
sys.stderr.write("Path: %s (depth=%s)\n" % (self.name, self.depth))
if recurse:
child_names = self.children.keys()
child_names.sort(svn_path_compare_paths)
for child_name in child_names:
self.children[child_name].dump(recurse, indent + 2)
class SubversionViewspec:
"""A representation of a Subversion sparse checkout specification."""
def __init__(self, base_url, revision, tree):
self.base_url = base_url # base URL of the checkout
self.revision = revision # revision of the checkout (-1 == HEAD)
self.tree = tree # the top-most TreeNode item
def svn_path_compare_paths(path1, path2):
"""Compare PATH1 and PATH2 as paths, sorting depth-first-ily.
NOTE: Stolen unapologetically from Subversion's Python bindings
module svn.core."""
path1_len = len(path1);
path2_len = len(path2);
min_len = min(path1_len, path2_len)
i = 0
# Are the paths exactly the same?
if path1 == path2:
return 0
# Skip past common prefix
while (i < min_len) and (path1[i] == path2[i]):
i = i + 1
# Children of paths are greater than their parents, but less than
# greater siblings of their parents
char1 = '\0'
char2 = '\0'
if (i < path1_len):
char1 = path1[i]
if (i < path2_len):
char2 = path2[i]
if (char1 == '/') and (i == path2_len):
return 1
if (char2 == '/') and (i == path1_len):
return -1
if (i < path1_len) and (char1 == '/'):
return -1
if (i < path2_len) and (char2 == '/'):
return 1
# Common prefix was skipped above, next character is compared to
# determine order
return cmp(char1, char2)
def parse_viewspec_headers(viewspec_fp):
"""Parse the headers from the viewspec file, return them as a
dictionary mapping header names to values."""
headers = {}
while 1:
line = viewspec_fp.readline().strip()
if not line:
break
name, value = [x.strip() for x in line.split(':', 1)]
headers[name] = value
return headers
def parse_viewspec(viewspec_fp):
"""Parse the viewspec file, returning a SubversionViewspec object
that represents the specification."""
headers = parse_viewspec_headers(viewspec_fp)
format = headers['Format']
assert format == '1'
base_url = headers['Url']
revision = int(headers.get('Revision', -1))
root_depth = DEPTH_EMPTY
rules = {}
while 1:
line = viewspec_fp.readline()
if not line:
break
line = line.rstrip()
# These are special rules for the top-most dir; don't fall thru.
if line == '**':
root_depth = DEPTH_INFINITY
continue
elif line == '*':
root_depth = DEPTH_IMMEDIATES
continue
elif line == '~':
root_depth = DEPTH_FILES
continue
# These are the regular per-path rules.
elif line[-3:] == '/**':
depth = DEPTH_INFINITY
path = line[:-3]
elif line[-2:] == '/*':
depth = DEPTH_IMMEDIATES
path = line[:-2]
elif line[-2:] == '/~':
depth = DEPTH_FILES
path = line[:-2]
else:
depth = DEPTH_EMPTY
path = line
# Add our rule to the set thereof.
assert not rules.has_key(path)
rules[path] = depth
tree = TreeNode('', root_depth)
paths = rules.keys()
paths.sort(svn_path_compare_paths)
for path in paths:
depth = rules[path]
path_parts = filter(None, path.split('/'))
tree_ptr = tree
for part in path_parts[:-1]:
child_node = tree_ptr.children.get(part, None)
if not child_node:
child_node = TreeNode(part, DEPTH_EMPTY)
tree_ptr.add_child(child_node)
tree_ptr = child_node
tree_ptr.add_child(TreeNode(path_parts[-1], depth))
return SubversionViewspec(base_url, revision, tree)
def checkout_tree(base_url, revision, tree_node, target_dir, is_top=True):
"""Checkout from BASE_URL, and into TARGET_DIR, the TREE_NODE
sparse checkout item. IS_TOP is set iff this node represents the
root of the checkout tree. REVISION is the revision to checkout,
or -1 if checking out HEAD."""
depth = tree_node.depth
revision_str = ''
if revision != -1:
revision_str = "--revision=%d " % (revision)
if is_top:
os.system('svn checkout "%s" "%s" --depth=%s %s'
% (base_url, target_dir, depth, revision_str))
else:
os.system('svn update "%s" --set-depth=%s %s'
% (target_dir, depth, revision_str))
child_names = tree_node.children.keys()
child_names.sort(svn_path_compare_paths)
for child_name in child_names:
checkout_tree(base_url + '/' + child_name,
revision,
tree_node.children[child_name],
os.path.join(target_dir, urllib.unquote(child_name)),
False)
def checkout_spec(viewspec, target_dir):
"""Checkout the view specification VIEWSPEC into TARGET_DIR."""
checkout_tree(viewspec.base_url,
viewspec.revision,
viewspec.tree,
target_dir)
def usage_and_exit(errmsg=None):
stream = errmsg and sys.stderr or sys.stdout
msg = __doc__.replace("__SCRIPTNAME__", os.path.basename(sys.argv[0]))
stream.write(msg)
if errmsg:
stream.write("ERROR: %s\n" % (errmsg))
sys.exit(errmsg and 1 or 0)
def main():
argc = len(sys.argv)
if argc < 2:
usage_and_exit('Not enough arguments.')
subcommand = sys.argv[1]
if subcommand == 'help':
usage_and_exit()
elif subcommand == 'help-format':
msg = FORMAT_HELP.replace("__SCRIPTNAME__",
os.path.basename(sys.argv[0]))
sys.stdout.write(msg)
elif subcommand == 'examine':
if argc < 3:
usage_and_exit('No viewspec file specified.')
fp = (sys.argv[2] == '-') and sys.stdin or open(sys.argv[2], 'r')
viewspec = parse_viewspec(fp)
sys.stdout.write("Url: %s\n" % (viewspec.base_url))
revision = viewspec.revision
if revision != -1:
sys.stdout.write("Revision: %s\n" % (revision))
else:
sys.stdout.write("Revision: HEAD\n")
sys.stdout.write("\n")
viewspec.tree.dump(True)
elif subcommand == 'checkout':
if argc < 3:
usage_and_exit('No viewspec file specified.')
if argc < 4:
usage_and_exit('No target directory specified.')
fp = (sys.argv[2] == '-') and sys.stdin or open(sys.argv[2], 'r')
checkout_spec(parse_viewspec(fp), sys.argv[3])
else:
usage_and_exit('Unknown subcommand "%s".' % (subcommand))
if __name__ == "__main__":
main()