1 ''' 2 Copyright 2011 Google Inc. 3 4 Use of this source code is governed by a BSD-style license that can be 5 found in the LICENSE file. 6 ''' 7 8 import fnmatch 9 import os 10 import re 11 import subprocess 12 import threading 13 14 PROPERTY_MIMETYPE = 'svn:mime-type' 15 16 # Status types for GetFilesWithStatus() 17 STATUS_ADDED = 0x01 18 STATUS_DELETED = 0x02 19 STATUS_MODIFIED = 0x04 20 STATUS_NOT_UNDER_SVN_CONTROL = 0x08 21 22 23 if os.name == 'nt': 24 SVN = 'svn.bat' 25 else: 26 SVN = 'svn' 27 28 29 def Cat(svn_url): 30 """Returns the contents of the file at the given svn_url. 31 32 @param svn_url URL of the file to read 33 """ 34 proc = subprocess.Popen([SVN, 'cat', svn_url], 35 stdout=subprocess.PIPE, 36 stderr=subprocess.STDOUT) 37 exitcode = proc.wait() 38 if not exitcode == 0: 39 raise Exception('Could not retrieve %s. Verify that the URL is valid ' 40 'and check your connection.' % svn_url) 41 return proc.communicate()[0] 42 43 44 class Svn: 45 46 def __init__(self, directory): 47 """Set up to manipulate SVN control within the given directory. 48 49 The resulting object is thread-safe: access to all methods is 50 synchronized (if one thread is currently executing any of its methods, 51 all other threads must wait before executing any of its methods). 52 53 @param directory 54 """ 55 self._directory = directory 56 # This must be a reentrant lock, so that it can be held by both 57 # _RunCommand() and (some of) the methods that call it. 58 self._rlock = threading.RLock() 59 60 def _RunCommand(self, args): 61 """Run a command (from self._directory) and return stdout as a single 62 string. 63 64 @param args a list of arguments 65 """ 66 with self._rlock: 67 print 'RunCommand: %s' % args 68 proc = subprocess.Popen(args, cwd=self._directory, 69 stdout=subprocess.PIPE, 70 stderr=subprocess.PIPE) 71 (stdout, stderr) = proc.communicate() 72 if proc.returncode is not 0: 73 raise Exception('command "%s" failed in dir "%s": %s' % 74 (args, self._directory, stderr)) 75 return stdout 76 77 def GetInfo(self): 78 """Run "svn info" and return a dictionary containing its output. 79 """ 80 output = self._RunCommand([SVN, 'info']) 81 svn_info = {} 82 for line in output.split('\n'): 83 if ':' in line: 84 (key, value) = line.split(':', 1) 85 svn_info[key.strip()] = value.strip() 86 return svn_info 87 88 def Checkout(self, url, path): 89 """Check out a working copy from a repository. 90 Returns stdout as a single string. 91 92 @param url URL from which to check out the working copy 93 @param path path (within self._directory) where the local copy will be 94 written 95 """ 96 return self._RunCommand([SVN, 'checkout', url, path]) 97 98 def Update(self, path): 99 """Update the working copy. 100 Returns stdout as a single string. 101 102 @param path path (within self._directory) within which to run 103 "svn update" 104 """ 105 return self._RunCommand([SVN, 'update', path]) 106 107 def ListSubdirs(self, url): 108 """Returns a list of all subdirectories (not files) within a given SVN 109 url. 110 111 @param url remote directory to list subdirectories of 112 """ 113 subdirs = [] 114 filenames = self._RunCommand([SVN, 'ls', url]).split('\n') 115 for filename in filenames: 116 if filename.endswith('/'): 117 subdirs.append(filename.strip('/')) 118 return subdirs 119 120 def GetNewFiles(self): 121 """Return a list of files which are in this directory but NOT under 122 SVN control. 123 """ 124 return self.GetFilesWithStatus(STATUS_NOT_UNDER_SVN_CONTROL) 125 126 def GetNewAndModifiedFiles(self): 127 """Return a list of files in this dir which are newly added or modified, 128 including those that are not (yet) under SVN control. 129 """ 130 return self.GetFilesWithStatus( 131 STATUS_ADDED | STATUS_MODIFIED | STATUS_NOT_UNDER_SVN_CONTROL) 132 133 def GetFilesWithStatus(self, status): 134 """Return a list of files in this dir with the given SVN status. 135 136 @param status bitfield combining one or more STATUS_xxx values 137 """ 138 status_types_string = '' 139 if status & STATUS_ADDED: 140 status_types_string += 'A' 141 if status & STATUS_DELETED: 142 status_types_string += 'D' 143 if status & STATUS_MODIFIED: 144 status_types_string += 'M' 145 if status & STATUS_NOT_UNDER_SVN_CONTROL: 146 status_types_string += '\?' 147 status_regex_string = '^[%s].....\s+(.+)$' % status_types_string 148 stdout = self._RunCommand([SVN, 'status']).replace('\r', '') 149 status_regex = re.compile(status_regex_string, re.MULTILINE) 150 files = status_regex.findall(stdout) 151 return files 152 153 def AddFiles(self, filenames): 154 """Adds these files to SVN control. 155 156 @param filenames files to add to SVN control 157 """ 158 self._RunCommand([SVN, 'add'] + filenames) 159 160 def SetProperty(self, filenames, property_name, property_value): 161 """Sets a svn property for these files. 162 163 @param filenames files to set property on 164 @param property_name property_name to set for each file 165 @param property_value what to set the property_name to 166 """ 167 if filenames: 168 self._RunCommand( 169 [SVN, 'propset', property_name, property_value] + filenames) 170 171 def SetPropertyByFilenamePattern(self, filename_pattern, 172 property_name, property_value): 173 """Sets a svn property for all files matching filename_pattern. 174 175 @param filename_pattern set the property for all files whose names match 176 this Unix-style filename pattern (e.g., '*.jpg') 177 @param property_name property_name to set for each file 178 @param property_value what to set the property_name to 179 """ 180 with self._rlock: 181 all_files = os.listdir(self._directory) 182 matching_files = sorted(fnmatch.filter(all_files, filename_pattern)) 183 self.SetProperty(matching_files, property_name, property_value) 184 185 def ExportBaseVersionOfFile(self, file_within_repo, dest_path): 186 """Retrieves a copy of the base version (what you would get if you ran 187 'svn revert') of a file within the repository. 188 189 @param file_within_repo path to the file within the repo whose base 190 version you wish to obtain 191 @param dest_path destination to which to write the base content 192 """ 193 self._RunCommand([SVN, 'export', '--revision', 'BASE', '--force', 194 file_within_repo, dest_path]) 195