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
22 A few standalone commands are also implemented to extract data from
23 arbitrary remote repositories.
27 # alphabetical order by last name, please
28 '"David Anderson" <dave@natulte.net>',
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.
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.
49 assert os
.path
.isabs(dest_path
)
50 if os
.path
.exists(dest_path
):
51 raise error
.ObstructionError('Cannot export to obstructed path %s' %
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.
71 url: The repository URL of the tag to examine.
74 output
= util
.run(['svn', 'log', '-q', '--stop-on-copy', url
],
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:])
83 def diff(url
, revision
):
84 """Retrieve a revision from a remote repository as a unified diff.
87 url: The repository URL on which to perform the diff.
88 revision: The revision to extract at the given url.
91 A string containing the changes extracted from the remote
92 repository, in unified diff format suitable for application using
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
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
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.
130 path: The working copy path to scan.
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
142 for line
in self
.status(path
):
146 unknown
.append(line
[7:])
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.
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.
165 path: The working copy path to update.
166 depth: If set, change the depth of the path before updating.
170 util
.run(['svn', 'update', self
.path(path
)])
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
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
]
187 # rm -rf makes me uneasy. Verify that all paths to be deleted
188 # are within the release working copy.
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
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.
210 src: The source working copy path.
211 dst: The destination working copy path.
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.
220 prop_name: The property name, eg. 'svn:externals'.
221 path: The working copy path on which the property is set.
224 return util
.run(['svn', 'propget', prop_name
, self
.path(path
)],
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.
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.
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.
246 paths: The list of working copy paths to add.
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.
258 paths: The list of working copy paths to delete.
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
271 path: The path to examine.
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
286 path: The path under which unknown/missing files should be
290 unknown
, missing
= self
._unknownAndMissing
(path
)
296 def commit(self
, message
, path
=''):
297 """Commit scheduled changes to the source repository.
300 message: The commit message to use.
301 path: The path to commit.
304 util
.run(['svn', 'commit', '-m', message
, self
.path(path
)])