Added tag v0-5-20090608 for changeset 19e9fdde919e
[Melange.git] / scripts / release / subversion.py
blobe84e620566b74c096c646447e36cc99c183fb4c8
1 # Copyright 2009 the Melange authors.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 """Subversion commandline wrapper.
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.
22 A few standalone commands are also implemented to extract data from
23 arbitrary remote repositories.
24 """
26 __authors__ = [
27 # alphabetical order by last name, please
28 '"David Anderson" <dave@natulte.net>',
32 import error
33 import util
36 def export(url, revision, dest_path):
37 """Export the contents of a repository to a local path.
39 Note that while the underlying 'svn export' only requires a URL,
40 we require that both a URL and a revision be specified, to fully
41 qualify the data to export.
43 Args:
44 url: The repository URL to export.
45 revision: The revision to export.
46 dest_path: The destination directory for the export. Note that
47 this is an absolute path, NOT a working copy relative 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])
56 def find_tag_rev(url):
57 """Return the revision at which a remote tag was created.
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.
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.
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
83 def diff(url, revision):
84 """Retrieve a revision from a remote repository as a unified diff.
86 Args:
87 url: The repository URL on which to perform the diff.
88 revision: The revision to extract at the given url.
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)
103 class WorkingCopy(util.Paths):
104 """Wrapper for operations on a Subversion working copy.
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.
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).
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.
123 def __init__(self, wc_dir):
124 util.Paths.__init__(self, wc_dir)
126 def _unknownAndMissing(self, path):
127 """Returns lists of unknown and missing files in the working copy.
129 Args:
130 path: The working copy path to scan.
132 Returns:
133 Two lists. The first is a list of all unknown paths
134 (subversion has no knowledge of them), the second is a list
135 of missing paths (subversion knows about them, but can't
136 find them). Paths in either list are relative to the input
137 path.
139 assert self.exists()
140 unknown = []
141 missing = []
142 for line in self.status(path):
143 if not line.strip():
144 continue
145 if line[0] == '?':
146 unknown.append(line[7:])
147 elif line[0] == '!':
148 missing.append(line[7:])
149 return unknown, missing
151 def checkout(self, url, depth='infinity'):
152 """Check out a working copy from the given URL.
154 Args:
155 url: The Subversion repository URL to check out.
156 depth: The depth of the working copy root.
158 assert not self.exists()
159 util.run(['svn', 'checkout', '--depth=' + depth, url, self.path()])
161 def update(self, path='', depth=None):
162 """Update a working copy path, optionally changing depth.
164 Args:
165 path: The working copy path to update.
166 depth: If set, change the depth of the path before updating.
168 assert self.exists()
169 if depth is None:
170 util.run(['svn', 'update', self.path(path)])
171 else:
172 util.run(['svn', 'update', '--set-depth=' + depth, self.path(path)])
174 def revert(self, path=''):
175 """Recursively revert a working copy path.
177 Note that this command is more zealous than the 'svn revert'
178 command, as it will also delete any files which subversion
179 does not know about.
181 util.run(['svn', 'revert', '-R', self.path(path)])
183 unknown, missing = self._unknownAndMissing(path)
184 unknown = [os.path.join(self.path(path), p) for p in unknown]
186 if unknown:
187 # rm -rf makes me uneasy. Verify that all paths to be deleted
188 # are within the release working copy.
189 for p in unknown:
190 assert p.startswith(self.path())
192 util.run(['rm', '-rf', '--'] + unknown)
194 def ls(self, dir=''):
195 """List the contents of a working copy directory.
197 Note that this returns the contents of the directory as seen
198 by the server, not constrained by the depth settings of the
199 local path.
201 assert self.exists()
202 return util.run(['svn', 'ls', self.path(dir)], capture=True)
204 def copy(self, src, dest):
205 """Copy a working copy path.
207 The copy is only scheduled for commit, not committed.
209 Args:
210 src: The source working copy path.
211 dst: The destination working copy path.
213 assert self.exists()
214 util.run(['svn', 'cp', self.path(src), self.path(dest)])
216 def propget(self, prop_name, path):
217 """Get the value of a property on a working copy path.
219 Args:
220 prop_name: The property name, eg. 'svn:externals'.
221 path: The working copy path on which the property is set.
223 assert self.exists()
224 return util.run(['svn', 'propget', prop_name, self.path(path)],
225 capture=True)
227 def propset(self, prop_name, prop_value, path):
228 """Set the value of a property on a working copy path.
230 The property change is only scheduled for commit, not committed.
232 Args:
233 prop_name: The property name, eg. 'svn:externals'.
234 prop_value: The value that should be set.
235 path: The working copy path on which to set the property.
237 assert self.exists()
238 util.run(['svn', 'propset', prop_name, prop_value, self.path(path)])
240 def add(self, paths):
241 """Schedule working copy paths for addition.
243 The paths are only scheduled for addition, not committed.
245 Args:
246 paths: The list of working copy paths to add.
248 assert self.exists()
249 paths = [self.path(p) for p in paths]
250 util.run(['svn', 'add'] + paths)
252 def remove(self, paths):
253 """Schedule working copy paths for deletion.
255 The paths are only scheduled for deletion, not committed.
257 Args:
258 paths: The list of working copy paths to delete.
260 assert self.exists()
261 paths = [self.path(p) for p in paths]
262 util.run(['svn', 'rm'] + paths)
264 def status(self, path=''):
265 """Return the status of a working copy path.
267 The status returned is the verbatim output of 'svn status' on
268 the path.
270 Args:
271 path: The path to examine.
273 assert self.exists()
274 return util.run(['svn', 'status', self.path(path)], capture=True)
276 def addRemove(self, path=''):
277 """Perform an "addremove" operation a working copy path.
279 An "addremove" runs 'svn status' and schedules all the unknown
280 paths (listed as '?') for addition, and all the missing paths
281 (listed as '!') for deletion. Its main use is to synchronize
282 working copy state after applying a patch in unified diff
283 format.
285 Args:
286 path: The path under which unknown/missing files should be
287 added/removed.
289 assert self.exists()
290 unknown, missing = self._unknownAndMissing(path)
291 if unknown:
292 self.add(unknown)
293 if missing:
294 self.remove(missing)
296 def commit(self, message, path=''):
297 """Commit scheduled changes to the source repository.
299 Args:
300 message: The commit message to use.
301 path: The path to commit.
303 assert self.exists()
304 util.run(['svn', 'commit', '-m', message, self.path(path)])