Merge with Pawel's head.
[Melange.git] / scripts / svn_helper.py
blob195db1b8fbfb8b3690d88c5ed71e6a8f206969c2
1 #!/usr/bin/python2.5
3 # Copyright 2008 the Melange authors.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Helper functions that wrap the pysvn Python svn bindings.
19 ls(): returns list of selected directory entries from an SVN repository
20 lsDirs(): wrapper around ls() that only returns node_kind.dir entries
21 lsFiles(): wrapper around ls() that only returns node_kind.files entries
22 exists(): returns True if repo_path exists in the svn repository
24 PYSVN_ALL_NODE_KINDS: all directory entry node_kinds supported by pysvn
25 PYSVN_FILE_DIR_NODE_KINDS: actual file and directory node_kinds
27 This module requires that the pysvn module be installed.
28 """
30 __authors__ = [
31 # alphabetical order by last name, please
32 '"Todd Larsen" <tlarsen@google.com>',
36 import os
37 import pysvn
39 # top level script needs to use a relative import
40 import settings
43 #: all of the directory entry node_kinds supported py pysvn
44 PYSVN_ALL_NODE_KINDS = set([pysvn.node_kind.none, pysvn.node_kind.dir,
45 pysvn.node_kind.file, pysvn.node_kind.unknown])
47 #: actual file and directory node_kinds (includes dir and file, but excludes
48 #: the "non-file" none and unknown)
49 PYSVN_FILE_DIR_NODE_KINDS = set([pysvn.node_kind.dir, pysvn.node_kind.file])
52 # pysvn Client object initialized lazily the first time getPySvnClient()
53 # is called.
54 _client = None
57 def getPySvnClient():
58 """Returns the module-global pysvn Client object (creating one if needed).
60 Lazily initializes a global pysvn Client object, returning the same one
61 once it exists.
62 """
63 global _client
65 if not _client:
66 _client = pysvn.Client()
68 return _client
71 def formatDirPath(path):
72 """Appends trailing separator to non-empty path if it is missing.
74 Args:
75 path: path string, which may be with or without a trailing separator,
76 or even empty or None
78 Returns:
79 path unchanged if path evaluates to False or already ends with a trailing
80 separator; otherwise, a / separator is appended
81 """
82 if path and not path.endswith('/'):
83 path = path + '/'
84 return path
87 def formatDirPaths(*args):
88 """Apply formatDirPath() to all supplied arguments, returning them in order.
89 """
90 return tuple([formatDirPath(arg) for arg in args])
93 def getCanonicalSvnPath(path):
94 """Returns the supplied svn repo path *without* the trailing / character.
96 Some pysvn methods raise exceptions if svn directory URLs end with a
97 trailing / ("non-canonical form") and some do not. Go figure...
98 """
99 if path and path.endswith('/'):
100 path = path[:-1]
101 return path
104 def useLocalOsSep(path):
105 """Return path with all / characters replaced with os.sep, to be OS-agnostic.
107 Args:
108 path: an SVN path (either working copy path or relative path, but not a
109 full repository URL) that uses the canonical / separators
111 return path.replace('/', os.sep)
114 def getExpandedWorkingCopyPath(path, wc_root=None):
115 """Returns expanded, local, native filesystem working copy path.
117 Args:
118 path: path to expand and convert to local filesystem directory separators
119 wc_root: if present, prepended to path first
121 path = useLocalOsSep(path)
123 if wc_root:
124 # prepend (Windows-compatible) working copy root if one was supplied
125 path = os.path.join(useLocalOsSep(wc_root), path)
127 path = settings.getExpandedPath(path)
129 if not path.endswith(os.sep):
130 path = path + os.sep
132 return path
135 def encodeRevision(rev):
136 """Encode supplied revision into a pysvn.Revision instance.
138 This function is currently very simplistic and does not produce all possible
139 types of pysvn.Revision object. See below for current limitations.
141 Args:
142 rev: integer revision number or None
144 Returns:
145 HEAD pysvn.Revision object if rev is None,
146 otherwise a pysvn.opt_revision_kind.number pysvn.Revision object created
147 using the supplied integer revision number
149 if rev is None:
150 return pysvn.Revision(pysvn.opt_revision_kind.head)
152 return pysvn.Revision(pysvn.opt_revision_kind.number, int(rev))
155 def ls(repo_path, client=None, keep_kinds=PYSVN_FILE_DIR_NODE_KINDS, **kwargs):
156 """Returns a list of (possibly recursive) svn repo directory entries.
158 Args:
159 repo_path: absolute svn repository path URL, including the server and
160 directory path within the repo
161 client: pysvn Client instance; default is None, which will use the pysvn
162 Client created by first call to getPySvnClient() (or create one if
163 necessary)
164 keep_kinds: types of directory entries to keep in the returned list; a
165 collection of pysvn.node_kind objects; default is
166 PYSVN_FILE_DIR_NODE_KINDS
167 **kwargs: keyword arguments passed on to Client.list(), including:
168 recurse: indicates if return results should include entries from
169 subdirectories of repo_path as well; default is False
171 Returns:
172 list of (Unicode, coming from pysvn) strings representing the entries
173 of types indicated by keep_kinds; strings are altered to match the
174 output of the actual 'svn ls' command: repo_path prefix is removed,
175 directories end with the / separator.
177 if not client:
178 client = getPySvnClient()
180 raw_entries = client.list(repo_path, **kwargs)
181 entries = []
183 # Find shortest repos_path that is a 'dir' entry; will be prefix of all
184 # other entries, since Client.list() always returns repo_path as one of
185 # the entries. It is easier and more reliable to do this search than to
186 # try to manipulate repo_path into the prefix (since the user could supply
187 # any number of valid, but different, formats).
188 shortest_path = raw_entries[0][0].repos_path
190 for svn_list, _ in raw_entries:
191 if svn_list.kind == pysvn.node_kind.dir:
192 entry_path = svn_list.repos_path
194 if len(entry_path) < len(shortest_path):
195 shortest_path = entry_path
197 # normalize the path name of entry_prefix to include a trailing separator
198 entry_prefix = formatDirPath(shortest_path)
200 for svn_list,_ in raw_entries:
201 # only include requested node kinds (dir, file, etc.)
202 if svn_list.kind not in keep_kinds:
203 continue
205 entry_path = svn_list.repos_path
207 # omit the repo_path directory entry itself (simiilar to omitting '.' as
208 # is done by the actual 'svn ls' command)
209 if entry_path == shortest_path:
210 continue
212 # all entry_paths except for the shortest should start with that
213 # shortest entry_prefix, so assert that and remove the prefix
214 assert entry_path.startswith(entry_prefix)
215 entry_path = entry_path[len(entry_prefix):]
217 # normalize directory entry_paths to include a trailing separator
218 if ((svn_list.kind == pysvn.node_kind.dir)
219 and (not entry_path.endswith('/'))):
220 entry_path = entry_path + '/'
222 entries.append(entry_path)
224 return entries
227 def lsDirs(repo_path, **kwargs):
228 """Wrapper around ls() that only returns node_kind.dir entries.
230 return ls(repo_path, keep_kinds=(pysvn.node_kind.dir,), **kwargs)
233 def lsFiles(repo_path, **kwargs):
234 """Wrapper around ls() that only returns node_kind.files entries.
236 return ls(repo_path, keep_kinds=(pysvn.node_kind.file,), **kwargs)
239 def exists(repo_path, client=None):
240 """Returns True if repo_path exists in the svn repository."""
241 if not client:
242 client = getPySvnClient()
244 try:
245 raw_entries = client.list(repo_path)
246 return True
247 except pysvn._pysvn.ClientError:
248 # Client.list() raises an exception if the path is not present in the repo
249 return False
252 def branchItems(src, dest, items, rev=None, client=None):
253 """Branch a list of items (files and/or directories).
255 Using the supplied pysvn client object, a list of items (expected to be
256 present in the src directory) is branched from the absolute svn repo src
257 path URL to the relative working client dest directory.
259 Args:
260 src: absolute svn repository source path URL, including the server and
261 directory path within the repo
262 dest: relative svn repository destination path in the current working copy
263 items: list of relative paths of items in src/ to branch to dest/ (no item
264 should begin with the / separator)
265 client: pysvn Client instance; default is None, which will use the pysvn
266 Client created by first call to getPySvnClient() (or create one if
267 necessary)
269 if not client:
270 client = getPySvnClient()
272 src = formatDirPath(src)
273 dest = useLocalOsSep(formatDirPath(dest))
275 for item in items:
276 assert not item.startswith('/')
277 src_item = getCanonicalSvnPath(src + item)
278 # attempt to be compatible with Windows working copy paths
279 item = useLocalOsSep(item)
280 client.copy(src_item, dest + item, src_revision=encodeRevision(rev))
283 def branchDir(src, dest, client=None, rev=None):
284 """Branch one directory to another.
286 Using the supplied pysvn client object, the absolute svn repo path URL src
287 directory is branched to the relative working client dest directory.
289 Args:
290 src: absolute svn repository source path URL, including the server and
291 directory path within the repo
292 dest: relative svn repository destination path in the current working copy
293 client: pysvn Client instance; default is None, which will use the pysvn
294 Client created by first call to getPySvnClient() (or create one if
295 necessary)
297 if not client:
298 client = getPySvnClient()
300 src = getCanonicalSvnPath(src)
301 dest = useLocalOsSep(formatDirPath(dest))
303 client.copy(src, dest, src_revision=encodeRevision(rev))
306 def exportItems(src, dest, items, rev=None, client=None):
307 """Export a list of items (files and/or directories).
309 Using the supplied pysvn client object, a list of items (expected to be
310 present in the src directory) is exported from the absolute svn repo src
311 path URL to the local filesystem directory.
313 Args:
314 src: absolute svn repository source path URL, including the server and
315 directory path within the repo
316 dest: local filesystem destination path
317 items: list of relative paths of items in src/ to export to dest/ (no item
318 should begin with the / separator)
319 client: pysvn Client instance; default is None, which will use the pysvn
320 Client created by first call to getPySvnClient() (or create one if
321 necessary)
323 if not client:
324 client = getPySvnClient()
326 src = formatDirPath(src)
327 dest = useLocalOsSep(formatDirPath(dest))
329 for item in items:
330 assert not item.startswith('/')
331 src_item = getCanonicalSvnPath(src + item)
332 # attempt to be compatible with Windows local filesystem paths
333 dest_item = useLocalOsSep(getCanonicalSvnPath(dest + item))
334 client.export(src_item, dest_item, revision=encodeRevision(rev))
337 def exportDir(src, dest, client=None, rev=None):
338 """Export one directory to another.
340 Using the supplied pysvn client object, the absolute svn repo path URL src
341 directory is exported to the the local filesystem directory.
343 Args:
344 src: absolute svn repository source path URL, including the server and
345 directory path within the repo
346 dest: local filesystem destination path
347 client: pysvn Client instance; default is None, which will use the pysvn
348 Client created by first call to getPySvnClient() (or create one if
349 necessary)
351 if not client:
352 client = getPySvnClient()
354 src = getCanonicalSvnPath(src)
355 dest = useLocalOsSep(getCanonicalSvnPath(dest))
357 client.export(src, dest, revision=encodeRevision(rev))