Home | History | Annotate | Download | only in tools
      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, revision='HEAD'):
     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         @param revision revision to update to
    105         """
    106         return self._RunCommand([SVN, 'update', path, '--revision', revision])
    107 
    108     def ListSubdirs(self, url):
    109         """Returns a list of all subdirectories (not files) within a given SVN
    110         url.
    111 
    112         @param url remote directory to list subdirectories of
    113         """
    114         subdirs = []
    115         filenames = self._RunCommand([SVN, 'ls', url]).split('\n')
    116         for filename in filenames:
    117             if filename.endswith('/'):
    118                 subdirs.append(filename.strip('/'))
    119         return subdirs
    120 
    121     def GetNewFiles(self):
    122         """Return a list of files which are in this directory but NOT under
    123         SVN control.
    124         """
    125         return self.GetFilesWithStatus(STATUS_NOT_UNDER_SVN_CONTROL)
    126 
    127     def GetNewAndModifiedFiles(self):
    128         """Return a list of files in this dir which are newly added or modified,
    129         including those that are not (yet) under SVN control.
    130         """
    131         return self.GetFilesWithStatus(
    132             STATUS_ADDED | STATUS_MODIFIED | STATUS_NOT_UNDER_SVN_CONTROL)
    133 
    134     def GetFilesWithStatus(self, status):
    135         """Return a list of files in this dir with the given SVN status.
    136 
    137         @param status bitfield combining one or more STATUS_xxx values
    138         """
    139         status_types_string = ''
    140         if status & STATUS_ADDED:
    141             status_types_string += 'A'
    142         if status & STATUS_DELETED:
    143             status_types_string += 'D'
    144         if status & STATUS_MODIFIED:
    145             status_types_string += 'M'
    146         if status & STATUS_NOT_UNDER_SVN_CONTROL:
    147             status_types_string += '\?'
    148         status_regex_string = '^[%s].....\s+(.+)$' % status_types_string
    149         stdout = self._RunCommand([SVN, 'status']).replace('\r', '')
    150         status_regex = re.compile(status_regex_string, re.MULTILINE)
    151         files = status_regex.findall(stdout)
    152         return files
    153 
    154     def AddFiles(self, filenames):
    155         """Adds these files to SVN control.
    156 
    157         @param filenames files to add to SVN control
    158         """
    159         self._RunCommand([SVN, 'add'] + filenames)
    160 
    161     def SetProperty(self, filenames, property_name, property_value):
    162         """Sets a svn property for these files.
    163 
    164         @param filenames files to set property on
    165         @param property_name property_name to set for each file
    166         @param property_value what to set the property_name to
    167         """
    168         if filenames:
    169             self._RunCommand(
    170                 [SVN, 'propset', property_name, property_value] + filenames)
    171 
    172     def SetPropertyByFilenamePattern(self, filename_pattern,
    173                                      property_name, property_value):
    174         """Sets a svn property for all files matching filename_pattern.
    175 
    176         @param filename_pattern set the property for all files whose names match
    177                this Unix-style filename pattern (e.g., '*.jpg')
    178         @param property_name property_name to set for each file
    179         @param property_value what to set the property_name to
    180         """
    181         with self._rlock:
    182             all_files = os.listdir(self._directory)
    183             matching_files = sorted(fnmatch.filter(all_files, filename_pattern))
    184             self.SetProperty(matching_files, property_name, property_value)
    185 
    186     def ExportBaseVersionOfFile(self, file_within_repo, dest_path):
    187         """Retrieves a copy of the base version (what you would get if you ran
    188         'svn revert') of a file within the repository.
    189 
    190         @param file_within_repo path to the file within the repo whose base
    191                version you wish to obtain
    192         @param dest_path destination to which to write the base content
    193         """
    194         self._RunCommand([SVN, 'export', '--revision', 'BASE', '--force',
    195                           file_within_repo, dest_path])
    196