| #!/usr/bin/env python |
| |
| # Copyright (c) 2005 Sony Pictures Imageworks Inc. All rights reserved. |
| # |
| # This software/script is free software; you may redistribute it |
| # and/or modify it under the terms of Version 2 or later of the GNU |
| # General Public License ("GPL") as published by the Free Software |
| # Foundation. |
| # |
| # This software/script is distributed "AS IS," WITHOUT ANY EXPRESS OR |
| # IMPLIED WARRANTIES OR REPRESENTATIONS OF ANY KIND WHATSOEVER, |
| # including without any implied warranty of MERCHANTABILITY or FITNESS |
| # FOR A PARTICULAR PURPOSE. See the GNU GPL (Version 2 or later) for |
| # details and license obligations. |
| |
| """ |
| Script to "export" from a Subversion repository a clean directory tree |
| of empty files instead of the content contained in those files in the |
| repository. The directory tree will also omit the .svn directories. |
| |
| The export is done from the repository specified by URL at HEAD into |
| PATH. If PATH is omitted, the last components of the URL is used for |
| the local directory name. If the --delete command line option is |
| given, then files and directories in PATH that do not exist in the |
| Subversion repository are deleted. |
| |
| As Subversion does not have any built-in tools to help locate files |
| and directories, in extremely large repositories it can be hard to |
| find what you are looking for. This script was written to create a |
| smaller non-working working copy that can be crawled with find or |
| find's locate utility to make it easier to find files. |
| |
| $HeadURL$ |
| $LastChangedRevision$ |
| $LastChangedDate$ |
| $LastChangedBy$ |
| """ |
| |
| import getopt |
| try: |
| my_getopt = getopt.gnu_getopt |
| except AttributeError: |
| my_getopt = getopt.getopt |
| import os |
| import sys |
| |
| import svn.client |
| import svn.core |
| |
| class context: |
| """A container for holding process context.""" |
| |
| def recursive_delete(dirname): |
| """Recursively delete the given directory name.""" |
| |
| for filename in os.listdir(dirname): |
| file_or_dir = os.path.join(dirname, filename) |
| if os.path.isdir(file_or_dir) and not os.path.islink(file_or_dir): |
| recursive_delete(file_or_dir) |
| else: |
| os.unlink(file_or_dir) |
| os.rmdir(dirname) |
| |
| def check_url_for_export(ctx, url, revision, client_ctx): |
| """Given a URL to a Subversion repository, check that the URL is |
| in the repository and that it refers to a directory and not a |
| non-directory.""" |
| |
| # Try to do a listing on the URL to see if the repository can be |
| # contacted. Do not catch failures here, as they imply that there |
| # is something wrong with the given URL. |
| try: |
| if ctx.verbose: |
| print("Trying to list '%s'" % url) |
| svn.client.ls(url, revision, 0, client_ctx) |
| |
| # Given a URL, the ls command does not tell you if |
| # you have a directory or a non-directory, so try doing a |
| # listing on the parent URL. If the listing on the parent URL |
| # fails, then assume that the given URL was the top of the |
| # repository and hence a directory. |
| try: |
| last_slash_index = url.rindex('/') |
| except ValueError: |
| print("Cannot find a / in the URL '%s'" % url) |
| return False |
| |
| parent_url = url[:last_slash_index] |
| path_name = url[last_slash_index+1:] |
| |
| try: |
| if ctx.verbose: |
| print("Trying to list '%s'" % parent_url) |
| remote_ls = svn.client.ls(parent_url, |
| revision, |
| 0, |
| client_ctx) |
| except svn.core.SubversionException: |
| if ctx.verbose: |
| print("Listing of '%s' failed, assuming URL is top of repos" \ |
| % parent_url) |
| return True |
| |
| try: |
| path_info = remote_ls[path_name] |
| except ValueError: |
| print("Able to ls '%s' but '%s' not in ls of '%s'" \ |
| % (url, path_name, parent_url)) |
| return False |
| |
| if svn.core.svn_node_dir != path_info.kind: |
| if ctx.verbose: |
| print("The URL '%s' is not a directory" % url) |
| return False |
| else: |
| if ctx.verbose: |
| print("The URL '%s' is a directory" % url) |
| return True |
| finally: |
| pass |
| |
| LOCAL_PATH_DIR = 'Directory' |
| LOCAL_PATH_NON_DIR = 'Non-directory' |
| LOCAL_PATH_NONE = 'Nonexistent' |
| def get_local_path_kind(pathname): |
| """Determine if there is a path in the filesystem and if the path |
| is a directory or non-directory.""" |
| |
| try: |
| os.stat(pathname) |
| if os.path.isdir(pathname): |
| status = LOCAL_PATH_DIR |
| else: |
| status = LOCAL_PATH_NON_DIR |
| except OSError: |
| status = LOCAL_PATH_NONE |
| |
| return status |
| |
| def synchronize_dir(ctx, url, dir_name, revision, client_ctx): |
| """Synchronize a directory given by a URL to a Subversion |
| repository with a local directory located by the dir_name |
| argument.""" |
| |
| status = True |
| |
| # Determine if there is a path in the filesystem and if the path |
| # is a directory or non-directory. |
| local_path_kind = get_local_path_kind(dir_name) |
| |
| # If the path on the local filesystem is not a directory, then |
| # delete it if deletes are enabled, otherwise return. |
| if LOCAL_PATH_NON_DIR == local_path_kind: |
| msg = ("'%s' which is a local non-directory but remotely a " + |
| "directory") % dir_name |
| if ctx.delete_local_paths: |
| print("Removing", msg) |
| os.unlink(dir_name) |
| local_path_kind = LOCAL_PATH_NONE |
| else: |
| print("Need to remove", msg) |
| ctx.delete_needed = True |
| return False |
| |
| if LOCAL_PATH_NONE == local_path_kind: |
| print("Creating directory '%s'" % dir_name) |
| os.mkdir(dir_name) |
| |
| remote_ls = svn.client.ls(url, |
| revision, |
| 0, |
| client_ctx) |
| |
| if ctx.verbose: |
| print("Syncing '%s' to '%s'" % (url, dir_name)) |
| |
| remote_pathnames = remote_ls.keys() |
| remote_pathnames.sort() |
| |
| local_pathnames = os.listdir(dir_name) |
| |
| for remote_pathname in remote_pathnames: |
| # For each name in the remote list, remove it from the local |
| # list so that the remaining names may be deleted. |
| try: |
| local_pathnames.remove(remote_pathname) |
| except ValueError: |
| pass |
| |
| full_remote_pathname = os.path.join(dir_name, remote_pathname) |
| |
| if remote_pathname in ctx.ignore_names or \ |
| full_remote_pathname in ctx.ignore_paths: |
| print("Skipping '%s'" % full_remote_pathname) |
| continue |
| |
| # Get the remote path kind. |
| remote_path_kind = remote_ls[remote_pathname].kind |
| |
| # If the remote path is a directory, then recursively handle |
| # that here. |
| if svn.core.svn_node_dir == remote_path_kind: |
| s = synchronize_dir(ctx, |
| os.path.join(url, remote_pathname), |
| full_remote_pathname, |
| revision, |
| client_ctx) |
| status &= s |
| |
| else: |
| # Determine if there is a path in the filesystem and if |
| # the path is a directory or non-directory. |
| local_path_kind = get_local_path_kind(full_remote_pathname) |
| |
| # If the path exists on the local filesystem but its kind |
| # does not match the kind in the Subversion repository, |
| # then either remove it if the local paths should be |
| # deleted or continue to the next path if deletes should |
| # not be done. |
| if LOCAL_PATH_DIR == local_path_kind: |
| msg = ("'%s' which is a local directory but remotely a " + |
| "non-directory") % full_remote_pathname |
| if ctx.delete_local_paths: |
| print("Removing", msg) |
| recursive_delete(full_remote_pathname) |
| local_path_kind = LOCAL_PATH_NONE |
| else: |
| print("Need to remove", msg) |
| ctx.delete_needed = True |
| continue |
| |
| if LOCAL_PATH_NONE == local_path_kind: |
| print("Creating file '%s'" % full_remote_pathname) |
| f = file(full_remote_pathname, 'w') |
| f.close() |
| |
| # Any remaining local paths should be removed. |
| local_pathnames.sort() |
| for local_pathname in local_pathnames: |
| full_local_pathname = os.path.join(dir_name, local_pathname) |
| if os.path.isdir(full_local_pathname): |
| if ctx.delete_local_paths: |
| print("Removing directory '%s'" % full_local_pathname) |
| recursive_delete(full_local_pathname) |
| else: |
| print("Need to remove directory '%s'" % full_local_pathname) |
| ctx.delete_needed = True |
| else: |
| if ctx.delete_local_paths: |
| print("Removing file '%s'" % full_local_pathname) |
| os.unlink(full_local_pathname) |
| else: |
| print("Need to remove file '%s'" % full_local_pathname) |
| ctx.delete_needed = True |
| |
| return status |
| |
| def main(ctx, url, export_pathname): |
| # Create a client context to run all Subversion client commands |
| # with. |
| client_ctx = svn.client.create_context() |
| |
| # Give the client context baton a suite of authentication |
| # providers. |
| providers = [ |
| svn.client.get_simple_provider(), |
| svn.client.get_ssl_client_cert_file_provider(), |
| svn.client.get_ssl_client_cert_pw_file_provider(), |
| svn.client.get_ssl_server_trust_file_provider(), |
| svn.client.get_username_provider(), |
| ] |
| client_ctx.auth_baton = svn.core.svn_auth_open(providers) |
| |
| # Load the configuration information from the configuration files. |
| client_ctx.config = svn.core.svn_config_get_config(None) |
| |
| # Use the HEAD revision to check out. |
| head_revision = svn.core.svn_opt_revision_t() |
| head_revision.kind = svn.core.svn_opt_revision_head |
| |
| # Check that the URL refers to a directory in the repository and |
| # not non-directory (file, special, etc). |
| status = check_url_for_export(ctx, url, head_revision, client_ctx) |
| if not status: |
| return 1 |
| |
| # Synchronize the current working directory with the given URL and |
| # descend recursively into the repository. |
| status = synchronize_dir(ctx, |
| url, |
| export_pathname, |
| head_revision, |
| client_ctx) |
| |
| if ctx.delete_needed: |
| print("There are files and directories in the local filesystem") |
| print("that do not exist in the Subversion repository that were") |
| sys.stdout.write("not deleted. ") |
| if ctx.delete_needed: |
| print("Please pass the --delete command line option") |
| print("to have this script delete those files and directories.") |
| else: |
| print("") |
| |
| if status: |
| return 0 |
| else: |
| return 1 |
| |
| def usage(verbose_usage): |
| message1 = \ |
| """usage: %s [options] URL [PATH] |
| Options include |
| --delete delete files and directories that don't exist in repos |
| -h (--help) show this message |
| -n (--name) arg add arg to the list of file or dir names to ignore |
| -p (--path) arg add arg to the list of file or dir paths to ignore |
| -v (--verbose) be verbose in output""" |
| |
| message2 = \ |
| """Script to "export" from a Subversion repository a clean directory tree |
| of empty files instead of the content contained in those files in the |
| repository. The directory tree will also omit the .svn directories. |
| |
| The export is done from the repository specified by URL at HEAD into |
| PATH. If PATH is omitted, the last components of the URL is used for |
| the local directory name. If the --delete command line option is |
| given, then files and directories in PATH that do not exist in the |
| Subversion repository are deleted. |
| |
| As Subversion does have any built-in tools to help locate files and |
| directories, in extremely large repositories it can be hard to find |
| what you are looking for. This script was written to create a smaller |
| non-working working copy that can be crawled with find or find's |
| locate utility to make it easier to find files.""" |
| |
| sys.stderr.write(message1 % sys.argv[0]) |
| sys.stderr.write("\n") |
| if verbose_usage: |
| sys.stderr.write(message2 + "\n") |
| sys.exit(1) |
| |
| if __name__ == '__main__': |
| ctx = context() |
| |
| # Context storing command line options settings. |
| ctx.delete_local_paths = False |
| ctx.ignore_names = [] |
| ctx.ignore_paths = [] |
| ctx.verbose = False |
| |
| # Context storing state from running the sync. |
| ctx.delete_needed = False |
| |
| try: |
| opts, args = my_getopt(sys.argv[1:], |
| 'hn:p:v', |
| ['delete', |
| 'help', |
| 'name=', |
| 'path=', |
| 'verbose' |
| ]) |
| except getopt.GetoptError: |
| usage(False) |
| if len(args) < 1 or len(args) > 2: |
| sys.stderr.write("Incorrect number of arguments\n") |
| usage(False) |
| |
| for o, a in opts: |
| if o in ('--delete',): |
| ctx.delete_local_paths = True |
| continue |
| if o in ('-h', '--help'): |
| usage(True) |
| continue |
| if o in ('-n', '--name'): |
| ctx.ignore_names += [a] |
| continue |
| if o in ('-p', '--path'): |
| ctx.ignore_paths += [a] |
| continue |
| if o in ('-v', '--verbose'): |
| ctx.verbose = True |
| continue |
| |
| # Get the URL to export and remove any trailing /'s from it. |
| url = args[0] |
| args = args[1:] |
| while url[-1] == '/': |
| url = url[:-1] |
| |
| # Get the local path to export into. |
| if args: |
| export_pathname = args[0] |
| args = args[1:] |
| else: |
| try: |
| last_slash_index = url.rindex('/') |
| except ValueError: |
| sys.stderr.write("Cannot find a / in the URL '%s'\n" % url) |
| usage(False) |
| export_pathname = url[last_slash_index+1:] |
| |
| sys.exit(main(ctx, url, export_pathname)) |