scripts/release/subversion.py
changeset 1849 f8728d5e2e07
child 1888 ef350db7f753
equal deleted inserted replaced
1848:a0cae3be1412 1849:f8728d5e2e07
       
     1 # Copyright 2009 the Melange authors.
       
     2 #
       
     3 # Licensed under the Apache License, Version 2.0 (the "License");
       
     4 # you may not use this file except in compliance with the License.
       
     5 # You may obtain a copy of the License at
       
     6 #
       
     7 #   http://www.apache.org/licenses/LICENSE-2.0
       
     8 #
       
     9 # Unless required by applicable law or agreed to in writing, software
       
    10 # distributed under the License is distributed on an "AS IS" BASIS,
       
    11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    12 # See the License for the specific language governing permissions and
       
    13 # limitations under the License.
       
    14 
       
    15 """Subversion commandline wrapper.
       
    16 
       
    17 This module provides access to a restricted subset of the Subversion
       
    18 commandline tool. The main functionality offered is an object wrapping
       
    19 a working copy, providing version control operations within that
       
    20 working copy.
       
    21 
       
    22 A few standalone commands are also implemented to extract data from
       
    23 arbitrary remote repositories.
       
    24 """
       
    25 
       
    26 __authors__ = [
       
    27     # alphabetical order by last name, please
       
    28     '"David Anderson" <dave@natulte.net>',
       
    29     ]
       
    30 
       
    31 import error
       
    32 import util
       
    33 
       
    34 
       
    35 def export(url, revision, dest_path):
       
    36   """Export the contents of a repository to a local path.
       
    37 
       
    38   Note that while the underlying 'svn export' only requires a URL, we
       
    39   require that both a URL and a revision be specified, to fully
       
    40   qualify the data to export.
       
    41 
       
    42   Args:
       
    43     url: The repository URL to export.
       
    44     revision: The revision to export.
       
    45     dest_path: The destination directory for the export. Note that
       
    46                this is an absolute path, NOT a working copy relative
       
    47                path.
       
    48     """
       
    49   assert os.path.isabs(dest_path)
       
    50   if os.path.exists(dest_path):
       
    51     raise error.ObstructionError('Cannot export to obstructed path %s' %
       
    52                                  dest_path)
       
    53   util.run(['svn', 'export', '-r', str(revision), url, dest_path])
       
    54 
       
    55 
       
    56 def find_tag_rev(url):
       
    57   """Return the revision at which a remote tag was created.
       
    58 
       
    59   Since tags are immutable by convention, usually the HEAD of a tag
       
    60   should be the tag creation revision. However, mistakes can happen,
       
    61   so this function will walk the history of the given tag URL,
       
    62   stopping on the first revision that was created by copy.
       
    63 
       
    64   This detection is not foolproof. For example: it will be fooled by a
       
    65   tag that was created, deleted, and recreated by copy at a different
       
    66   revision. It is not clear what the desired behavior in these edge
       
    67   cases are, and no attempt is made to handle them. You should request
       
    68   user confirmation before using the result of this function.
       
    69 
       
    70   Args:
       
    71     url: The repository URL of the tag to examine.
       
    72   """
       
    73   try:
       
    74     output = util.run(['svn', 'log', '-q', '--stop-on-copy', url],
       
    75                       capture=True)
       
    76   except util.SubprocessFailed:
       
    77     raise error.ExpectationFailed('No tag at URL ' + url)
       
    78   first_rev_line = output[-2]
       
    79   first_rev = int(first_rev_line.split()[0][1:])
       
    80   return first_rev
       
    81 
       
    82 
       
    83 def diff(url, revision):
       
    84   """Retrieve a revision from a remote repository as a unified diff.
       
    85 
       
    86   Args:
       
    87     url: The repository URL on which to perform the diff.
       
    88     revision: The revision to extract at the given url.
       
    89 
       
    90   Returns:
       
    91     A string containing the changes extracted from the remote
       
    92     repository, in unified diff format suitable for application using
       
    93     'patch'.
       
    94   """
       
    95   try:
       
    96     return util.run(['svn', 'diff', '-c', str(revision), url],
       
    97                     capture=True, split_capture=False)
       
    98   except util.SubprocessFailed:
       
    99     raise error.ExpectationFailed('Could not get diff for r%d '
       
   100                                   'from remote repository' % revision)
       
   101 
       
   102 
       
   103 class WorkingCopy(util.Paths):
       
   104   """Wrapper for operations on a Subversion working copy.
       
   105 
       
   106   An instance of this class is bound to a specific working copy
       
   107   directory, and provides an API to perform various Subversion
       
   108   operations on this working copy.
       
   109 
       
   110   Some methods take a 'depth' argument. Depth in Subversion is a
       
   111   feature that allows the creation of arbitrarily shallow or deep
       
   112   working copies on a per-directory basis. Possible values are
       
   113   'none' (no files or directories), 'files' (only files in .),
       
   114   'immediates' (files and directories in ., directories checked out
       
   115   at depth 'none') or 'infinity' (a normal working copy with
       
   116   everything).
       
   117 
       
   118   Note that this wrapper also doubles as a Paths object, offering an
       
   119   easy way to get or check the existence of paths in the working
       
   120   copy.
       
   121   """
       
   122 
       
   123   def __init__(self, wc_dir):
       
   124     util.Paths.__init__(self, wc_dir)
       
   125 
       
   126   def _unknownAndMissing(self, path):
       
   127     """Returns lists of unknown and missing files in the working copy.
       
   128 
       
   129     Args:
       
   130       path: The working copy path to scan.
       
   131 
       
   132     Returns:
       
   133 
       
   134       Two lists. The first is a list of all unknown paths
       
   135       (subversion has no knowledge of them), the second is a list
       
   136       of missing paths (subversion knows about them, but can't
       
   137       find them). Paths in either list are relative to the input
       
   138       path.
       
   139     """
       
   140     assert self.exists()
       
   141     unknown = []
       
   142     missing = []
       
   143     for line in self.status(path):
       
   144       if not line.strip():
       
   145         continue
       
   146       if line[0] == '?':
       
   147         unknown.append(line[7:])
       
   148       elif line[0] == '!':
       
   149         missing.append(line[7:])
       
   150     return unknown, missing
       
   151 
       
   152   def checkout(self, url, depth='infinity'):
       
   153     """Check out a working copy from the given URL.
       
   154 
       
   155     Args:
       
   156       url: The Subversion repository URL to check out.
       
   157       depth: The depth of the working copy root.
       
   158     """
       
   159     assert not self.exists()
       
   160     util.run(['svn', 'checkout', '--depth=' + depth, url, self.path()])
       
   161 
       
   162   def update(self, path='', depth=None):
       
   163     """Update a working copy path, optionally changing depth.
       
   164 
       
   165     Args:
       
   166       path: The working copy path to update.
       
   167       depth: If set, change the depth of the path before updating.
       
   168     """
       
   169     assert self.exists()
       
   170     if depth is None:
       
   171       util.run(['svn', 'update', self.path(path)])
       
   172     else:
       
   173       util.run(['svn', 'update', '--set-depth=' + depth, self.path(path)])
       
   174 
       
   175   def revert(self, path=''):
       
   176     """Recursively revert a working copy path.
       
   177 
       
   178     Note that this command is more zealous than the 'svn revert'
       
   179     command, as it will also delete any files which subversion
       
   180     does not know about.
       
   181     """
       
   182     util.run(['svn', 'revert', '-R', self.path(path)])
       
   183 
       
   184     unknown, missing = self._unknownAndMissing(path)
       
   185     unknown = [os.path.join(self.path(path), p) for p in unknown]
       
   186 
       
   187     if unknown:
       
   188       # rm -rf makes me uneasy. Verify that all paths to be deleted
       
   189       # are within the release working copy.
       
   190       for p in unknown:
       
   191         assert p.startswith(self.path())
       
   192 
       
   193       util.run(['rm', '-rf', '--'] + unknown)
       
   194 
       
   195   def ls(self, dir=''):
       
   196     """List the contents of a working copy directory.
       
   197 
       
   198     Note that this returns the contents of the directory as seen
       
   199     by the server, not constrained by the depth settings of the
       
   200     local path.
       
   201     """
       
   202     assert self.exists()
       
   203     return util.run(['svn', 'ls', self.path(dir)], capture=True)
       
   204 
       
   205   def copy(self, src, dest):
       
   206     """Copy a working copy path.
       
   207 
       
   208     The copy is only scheduled for commit, not committed.
       
   209 
       
   210     Args:
       
   211       src: The source working copy path.
       
   212       dst: The destination working copy path.
       
   213     """
       
   214     assert self.exists()
       
   215     util.run(['svn', 'cp', self.path(src), self.path(dest)])
       
   216 
       
   217   def propget(self, prop_name, path):
       
   218     """Get the value of a property on a working copy path.
       
   219 
       
   220     Args:
       
   221       prop_name: The property name, eg. 'svn:externals'.
       
   222       path: The working copy path on which the property is set.
       
   223     """
       
   224     assert self.exists()
       
   225     return util.run(['svn', 'propget', prop_name, self.path(path)],
       
   226             capture=True)
       
   227 
       
   228   def propset(self, prop_name, prop_value, path):
       
   229     """Set the value of a property on a working copy path.
       
   230 
       
   231     The property change is only scheduled for commit, not committed.
       
   232 
       
   233     Args:
       
   234       prop_name: The property name, eg. 'svn:externals'.
       
   235       prop_value: The value that should be set.
       
   236       path: The working copy path on which to set the property.
       
   237     """
       
   238     assert self.exists()
       
   239     util.run(['svn', 'propset', prop_name, prop_value, self.path(path)])
       
   240 
       
   241   def add(self, paths):
       
   242     """Schedule working copy paths for addition.
       
   243 
       
   244     The paths are only scheduled for addition, not committed.
       
   245 
       
   246     Args:
       
   247       paths: The list of working copy paths to add.
       
   248     """
       
   249     assert self.exists()
       
   250     paths = [self.path(p) for p in paths]
       
   251     util.run(['svn', 'add'] + paths)
       
   252 
       
   253   def remove(self, paths):
       
   254     """Schedule working copy paths for deletion.
       
   255 
       
   256     The paths are only scheduled for deletion, not committed.
       
   257 
       
   258     Args:
       
   259       paths: The list of working copy paths to delete.
       
   260     """
       
   261     assert self.exists()
       
   262     paths = [self.path(p) for p in paths]
       
   263     util.run(['svn', 'rm'] + paths)
       
   264 
       
   265   def status(self, path=''):
       
   266     """Return the status of a working copy path.
       
   267 
       
   268     The status returned is the verbatim output of 'svn status' on
       
   269     the path.
       
   270 
       
   271     Args:
       
   272       path: The path to examine.
       
   273     """
       
   274     assert self.exists()
       
   275     return util.run(['svn', 'status', self.path(path)], capture=True)
       
   276 
       
   277   def addRemove(self, path=''):
       
   278     """Perform an "addremove" operation a working copy path.
       
   279 
       
   280     An "addremove" runs 'svn status' and schedules all the unknown
       
   281     paths (listed as '?') for addition, and all the missing paths
       
   282     (listed as '!') for deletion. Its main use is to synchronize
       
   283     working copy state after applying a patch in unified diff
       
   284     format.
       
   285 
       
   286     Args:
       
   287       path: The path under which unknown/missing files should be
       
   288         added/removed.
       
   289     """
       
   290     assert self.exists()
       
   291     unknown, missing = self._unknownAndMissing(path)
       
   292     if unknown:
       
   293       self.add(unknown)
       
   294     if missing:
       
   295       self.remove(missing)
       
   296 
       
   297   def commit(self, message, path=''):
       
   298     """Commit scheduled changes to the source repository.
       
   299 
       
   300     Args:
       
   301       message: The commit message to use.
       
   302       path: The path to commit.
       
   303     """
       
   304     assert self.exists()
       
   305     util.run(['svn', 'commit', '-m', message, self.path(path)])