| ''' | 
 | Copyright 2011 Google Inc. | 
 |  | 
 | Use of this source code is governed by a BSD-style license that can be | 
 | found in the LICENSE file. | 
 | ''' | 
 |  | 
 | import fnmatch | 
 | import os | 
 | import re | 
 | import subprocess | 
 | import threading | 
 |  | 
 | PROPERTY_MIMETYPE = 'svn:mime-type' | 
 |  | 
 | # Status types for GetFilesWithStatus() | 
 | STATUS_ADDED                 = 0x01 | 
 | STATUS_DELETED               = 0x02 | 
 | STATUS_MODIFIED              = 0x04 | 
 | STATUS_NOT_UNDER_SVN_CONTROL = 0x08 | 
 |  | 
 |  | 
 | if os.name == 'nt': | 
 |   SVN = 'svn.bat' | 
 | else: | 
 |   SVN = 'svn' | 
 |  | 
 |  | 
 | def Cat(svn_url): | 
 |     """Returns the contents of the file at the given svn_url.  | 
 |  | 
 |     @param svn_url URL of the file to read | 
 |     """ | 
 |     proc = subprocess.Popen([SVN, 'cat', svn_url], | 
 |                             stdout=subprocess.PIPE, | 
 |                             stderr=subprocess.STDOUT) | 
 |     exitcode = proc.wait() | 
 |     if not exitcode == 0: | 
 |         raise Exception('Could not retrieve %s. Verify that the URL is valid ' | 
 |                         'and check your connection.' % svn_url) | 
 |     return proc.communicate()[0] | 
 |  | 
 |  | 
 | class Svn: | 
 |  | 
 |     def __init__(self, directory): | 
 |         """Set up to manipulate SVN control within the given directory. | 
 |  | 
 |         The resulting object is thread-safe: access to all methods is | 
 |         synchronized (if one thread is currently executing any of its methods, | 
 |         all other threads must wait before executing any of its methods). | 
 |  | 
 |         @param directory | 
 |         """ | 
 |         self._directory = directory | 
 |         # This must be a reentrant lock, so that it can be held by both | 
 |         # _RunCommand() and (some of) the methods that call it. | 
 |         self._rlock = threading.RLock() | 
 |  | 
 |     def _RunCommand(self, args): | 
 |         """Run a command (from self._directory) and return stdout as a single | 
 |         string. | 
 |  | 
 |         @param args a list of arguments | 
 |         """ | 
 |         with self._rlock: | 
 |             print 'RunCommand: %s' % args | 
 |             proc = subprocess.Popen(args, cwd=self._directory, | 
 |                                     stdout=subprocess.PIPE, | 
 |                                     stderr=subprocess.PIPE) | 
 |             (stdout, stderr) = proc.communicate() | 
 |             if proc.returncode is not 0: | 
 |               raise Exception('command "%s" failed in dir "%s": %s' % | 
 |                               (args, self._directory, stderr)) | 
 |             return stdout | 
 |  | 
 |     def GetInfo(self): | 
 |         """Run "svn info" and return a dictionary containing its output. | 
 |         """ | 
 |         output = self._RunCommand([SVN, 'info']) | 
 |         svn_info = {} | 
 |         for line in output.split('\n'): | 
 |           if ':' in line: | 
 |             (key, value) = line.split(':', 1) | 
 |             svn_info[key.strip()] = value.strip() | 
 |         return svn_info | 
 |  | 
 |     def Checkout(self, url, path): | 
 |         """Check out a working copy from a repository. | 
 |         Returns stdout as a single string. | 
 |  | 
 |         @param url URL from which to check out the working copy | 
 |         @param path path (within self._directory) where the local copy will be | 
 |         written | 
 |         """ | 
 |         return self._RunCommand([SVN, 'checkout', url, path]) | 
 |  | 
 |     def Update(self, path, revision='HEAD'): | 
 |         """Update the working copy. | 
 |         Returns stdout as a single string. | 
 |  | 
 |         @param path path (within self._directory) within which to run | 
 |           "svn update" | 
 |         @param revision revision to update to | 
 |         """ | 
 |         return self._RunCommand([SVN, 'update', path, '--revision', revision]) | 
 |  | 
 |     def ListSubdirs(self, url): | 
 |         """Returns a list of all subdirectories (not files) within a given SVN | 
 |         url. | 
 |  | 
 |         @param url remote directory to list subdirectories of | 
 |         """ | 
 |         subdirs = [] | 
 |         filenames = self._RunCommand([SVN, 'ls', url]).split('\n') | 
 |         for filename in filenames: | 
 |             if filename.endswith('/'): | 
 |                 subdirs.append(filename.strip('/')) | 
 |         return subdirs | 
 |  | 
 |     def GetNewFiles(self): | 
 |         """Return a list of files which are in this directory but NOT under | 
 |         SVN control. | 
 |         """ | 
 |         return self.GetFilesWithStatus(STATUS_NOT_UNDER_SVN_CONTROL) | 
 |  | 
 |     def GetNewAndModifiedFiles(self): | 
 |         """Return a list of files in this dir which are newly added or modified, | 
 |         including those that are not (yet) under SVN control. | 
 |         """ | 
 |         return self.GetFilesWithStatus( | 
 |             STATUS_ADDED | STATUS_MODIFIED | STATUS_NOT_UNDER_SVN_CONTROL) | 
 |  | 
 |     def GetFilesWithStatus(self, status): | 
 |         """Return a list of files in this dir with the given SVN status. | 
 |  | 
 |         @param status bitfield combining one or more STATUS_xxx values | 
 |         """ | 
 |         status_types_string = '' | 
 |         if status & STATUS_ADDED: | 
 |             status_types_string += 'A' | 
 |         if status & STATUS_DELETED: | 
 |             status_types_string += 'D' | 
 |         if status & STATUS_MODIFIED: | 
 |             status_types_string += 'M' | 
 |         if status & STATUS_NOT_UNDER_SVN_CONTROL: | 
 |             status_types_string += '\?' | 
 |         status_regex_string = '^[%s].....\s+(.+)$' % status_types_string | 
 |         stdout = self._RunCommand([SVN, 'status']).replace('\r', '') | 
 |         status_regex = re.compile(status_regex_string, re.MULTILINE) | 
 |         files = status_regex.findall(stdout) | 
 |         return files | 
 |  | 
 |     def AddFiles(self, filenames): | 
 |         """Adds these files to SVN control. | 
 |  | 
 |         @param filenames files to add to SVN control | 
 |         """ | 
 |         self._RunCommand([SVN, 'add'] + filenames) | 
 |  | 
 |     def SetProperty(self, filenames, property_name, property_value): | 
 |         """Sets a svn property for these files. | 
 |  | 
 |         @param filenames files to set property on | 
 |         @param property_name property_name to set for each file | 
 |         @param property_value what to set the property_name to | 
 |         """ | 
 |         if filenames: | 
 |             self._RunCommand( | 
 |                 [SVN, 'propset', property_name, property_value] + filenames) | 
 |  | 
 |     def SetPropertyByFilenamePattern(self, filename_pattern, | 
 |                                      property_name, property_value): | 
 |         """Sets a svn property for all files matching filename_pattern. | 
 |  | 
 |         @param filename_pattern set the property for all files whose names match | 
 |                this Unix-style filename pattern (e.g., '*.jpg') | 
 |         @param property_name property_name to set for each file | 
 |         @param property_value what to set the property_name to | 
 |         """ | 
 |         with self._rlock: | 
 |             all_files = os.listdir(self._directory) | 
 |             matching_files = sorted(fnmatch.filter(all_files, filename_pattern)) | 
 |             self.SetProperty(matching_files, property_name, property_value) | 
 |  | 
 |     def ExportBaseVersionOfFile(self, file_within_repo, dest_path): | 
 |         """Retrieves a copy of the base version (what you would get if you ran | 
 |         'svn revert') of a file within the repository. | 
 |  | 
 |         @param file_within_repo path to the file within the repo whose base | 
 |                version you wish to obtain | 
 |         @param dest_path destination to which to write the base content | 
 |         """ | 
 |         self._RunCommand([SVN, 'export', '--revision', 'BASE', '--force', | 
 |                           file_within_repo, dest_path]) |