Mercurial > hgsubversion
comparison svnwrap/svn_swig_wrapper.py @ 316:c3c647aff97c
Merge with danchr's changes.
author | Augie Fackler <durin42@gmail.com> |
---|---|
date | Sun, 03 May 2009 21:44:53 -0500 |
parents | 97360cb777a8 |
children | c4061e57974c |
comparison
equal
deleted
inserted
replaced
315:963d27a0b1c2 | 316:c3c647aff97c |
---|---|
4 import shutil | 4 import shutil |
5 import sys | 5 import sys |
6 import tempfile | 6 import tempfile |
7 import urlparse | 7 import urlparse |
8 import urllib | 8 import urllib |
9 import hashlib | |
10 import collections | |
9 | 11 |
10 from svn import client | 12 from svn import client |
11 from svn import core | 13 from svn import core |
12 from svn import delta | 14 from svn import delta |
13 from svn import ra | 15 from svn import ra |
14 | 16 |
17 from mercurial import util as hgutil | |
18 | |
15 def version(): | 19 def version(): |
16 return '%d.%d.%d' % (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) | 20 return '%d.%d.%d' % (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) |
17 | 21 |
18 if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) < (1, 5, 0): #pragma: no cover | 22 if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) < (1, 5, 0): #pragma: no cover |
19 raise ImportError, 'You must have Subversion 1.5.0 or newer and matching SWIG bindings.' | 23 raise ImportError, 'You must have Subversion 1.5.0 or newer and matching SWIG bindings.' |
23 """ | 27 """ |
24 | 28 |
25 class SubversionRepoCanNotDiff(Exception): | 29 class SubversionRepoCanNotDiff(Exception): |
26 """Exception raised when the svn API diff3() command cannot be used | 30 """Exception raised when the svn API diff3() command cannot be used |
27 """ | 31 """ |
32 | |
33 '''Default chunk size used in fetch_history_at_paths() and revisions().''' | |
34 _chunk_size = 1000 | |
28 | 35 |
29 def optrev(revnum): | 36 def optrev(revnum): |
30 optrev = core.svn_opt_revision_t() | 37 optrev = core.svn_opt_revision_t() |
31 optrev.kind = core.svn_opt_revision_number | 38 optrev.kind = core.svn_opt_revision_number |
32 optrev.value.number = revnum | 39 optrev.value.number = revnum |
33 return optrev | 40 return optrev |
34 | 41 |
35 svn_config = core.svn_config_get_config(None) | 42 svn_config = core.svn_config_get_config(None) |
36 class RaCallbacks(ra.Callbacks): | 43 class RaCallbacks(ra.Callbacks): |
37 def open_tmp_file(self, pool): #pragma: no cover | 44 @staticmethod |
45 def open_tmp_file(pool): #pragma: no cover | |
38 (fd, fn) = tempfile.mkstemp() | 46 (fd, fn) = tempfile.mkstemp() |
39 os.close(fd) | 47 os.close(fd) |
40 return fn | 48 return fn |
41 | 49 |
42 def get_client_string(self, pool): | 50 @staticmethod |
51 def get_client_string(pool): | |
43 return 'hgsubversion' | 52 return 'hgsubversion' |
44 | 53 |
45 | |
46 def user_pass_prompt(realm, default_username, ms, pool): #pragma: no cover | 54 def user_pass_prompt(realm, default_username, ms, pool): #pragma: no cover |
55 # FIXME: should use getpass() and username() from mercurial.ui | |
47 creds = core.svn_auth_cred_simple_t() | 56 creds = core.svn_auth_cred_simple_t() |
48 creds.may_save = ms | 57 creds.may_save = ms |
49 if default_username: | 58 if default_username: |
50 sys.stderr.write('Auth realm: %s\n' % (realm,)) | 59 sys.stderr.write('Auth realm: %s\n' % (realm,)) |
51 creds.username = default_username | 60 creds.username = default_username |
115 else: | 124 else: |
116 user = urllib.unquote(userpass) or None | 125 user = urllib.unquote(userpass) or None |
117 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) | 126 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) |
118 return (user, passwd, url) | 127 return (user, passwd, url) |
119 | 128 |
120 class Revision(object): | 129 class Revision(tuple): |
121 """Wrapper for a Subversion revision. | 130 """Wrapper for a Subversion revision. |
131 | |
132 Derives from tuple in an attempt to minimise the memory footprint. | |
122 """ | 133 """ |
123 def __init__(self, revnum, author, message, date, paths, strip_path=''): | 134 def __new__(self, revnum, author, message, date, paths, strip_path=''): |
124 self.revnum, self.author, self.message = revnum, author, message | 135 _paths = {} |
125 self.date = date | |
126 self.paths = {} | |
127 if paths: | 136 if paths: |
128 for p in paths: | 137 for p in paths: |
129 self.paths[p[len(strip_path):]] = paths[p] | 138 _paths[p[len(strip_path):]] = paths[p] |
139 return tuple.__new__(self, | |
140 (revnum, author, message, date, _paths)) | |
141 | |
142 def get_revnum(self): | |
143 return self[0] | |
144 revnum = property(get_revnum) | |
145 | |
146 def get_author(self): | |
147 return self[1] | |
148 author = property(get_author) | |
149 | |
150 def get_message(self): | |
151 return self[2] | |
152 message = property(get_message) | |
153 | |
154 def get_date(self): | |
155 return self[3] | |
156 date = property(get_date) | |
157 | |
158 def get_paths(self): | |
159 return self[4] | |
160 paths = property(get_paths) | |
130 | 161 |
131 def __str__(self): | 162 def __str__(self): |
132 return 'r%d by %s' % (self.revnum, self.author) | 163 return 'r%d by %s' % (self.revnum, self.author) |
133 | 164 |
134 _svntypes = { | 165 _svntypes = { |
140 """Wrapper for a Subversion repository. | 171 """Wrapper for a Subversion repository. |
141 | 172 |
142 This uses the SWIG Python bindings, and will only work on svn >= 1.4. | 173 This uses the SWIG Python bindings, and will only work on svn >= 1.4. |
143 It takes a required param, the URL. | 174 It takes a required param, the URL. |
144 """ | 175 """ |
145 def __init__(self, url='', username='', password=''): | 176 def __init__(self, url='', username='', password='', head=None): |
146 parsed = parse_url(url) | 177 parsed = parse_url(url) |
147 # --username and --password override URL credentials | 178 # --username and --password override URL credentials |
148 self.username = username or parsed[0] | 179 self.username = username or parsed[0] |
149 self.password = password or parsed[1] | 180 self.password = password or parsed[1] |
150 self.svn_url = parsed[2] | 181 self.svn_url = parsed[2] |
151 self.auth_baton_pool = core.Pool() | 182 self.auth_baton_pool = core.Pool() |
152 self.auth_baton = _create_auth_baton(self.auth_baton_pool) | 183 self.auth_baton = _create_auth_baton(self.auth_baton_pool) |
184 # self.init_ra_and_client() assumes that a pool already exists | |
185 self.pool = core.Pool() | |
153 | 186 |
154 self.init_ra_and_client() | 187 self.init_ra_and_client() |
155 self.uuid = ra.get_uuid(self.ra, self.pool) | 188 self.uuid = ra.get_uuid(self.ra, self.pool) |
156 repo_root = ra.get_repos_root(self.ra, self.pool) | 189 repo_root = ra.get_repos_root(self.ra, self.pool) |
157 # *will* have a leading '/', would not if we used get_repos_root2 | 190 # *will* have a leading '/', would not if we used get_repos_root2 |
179 self.client_context.auth_baton = self.auth_baton | 212 self.client_context.auth_baton = self.auth_baton |
180 self.client_context.config = svn_config | 213 self.client_context.config = svn_config |
181 callbacks = RaCallbacks() | 214 callbacks = RaCallbacks() |
182 callbacks.auth_baton = self.auth_baton | 215 callbacks.auth_baton = self.auth_baton |
183 self.callbacks = callbacks | 216 self.callbacks = callbacks |
184 self.ra = ra.open2(self.svn_url.encode('utf-8'), callbacks, | 217 try: |
185 svn_config, self.pool) | 218 self.ra = ra.open2(self.svn_url.encode('utf-8'), callbacks, |
219 svn_config, self.pool) | |
220 except core.SubversionException, e: | |
221 raise hgutil.Abort(e.args[0]) | |
186 | 222 |
187 def HEAD(self): | 223 def HEAD(self): |
188 return ra.get_latest_revnum(self.ra, self.pool) | 224 return ra.get_latest_revnum(self.ra, self.pool) |
189 HEAD = property(HEAD) | 225 HEAD = property(HEAD) |
190 | 226 |
191 def START(self): | 227 def START(self): |
192 return 0 | 228 return 0 |
193 START = property(START) | 229 START = property(START) |
194 | 230 |
231 def last_changed_rev(self): | |
232 try: | |
233 holder = [] | |
234 ra.get_log(self.ra, [''], | |
235 self.HEAD, 1, | |
236 1, #limit of how many log messages to load | |
237 True, # don't need to know changed paths | |
238 True, # stop on copies | |
239 lambda paths, revnum, author, date, message, pool: | |
240 holder.append(revnum), | |
241 self.pool) | |
242 | |
243 return holder[-1] | |
244 except core.SubversionException, e: | |
245 if e.apr_err not in [core.SVN_ERR_FS_NOT_FOUND]: | |
246 raise | |
247 else: | |
248 return self.HEAD | |
249 last_changed_rev = property(last_changed_rev) | |
250 | |
195 def branches(self): | 251 def branches(self): |
196 """Get the branches defined in this repo assuming a standard layout. | 252 """Get the branches defined in this repo assuming a standard layout. |
253 | |
254 This method should be eliminated; this class does not have | |
255 sufficient knowledge to yield all known tags. | |
197 """ | 256 """ |
198 branches = self.list_dir('branches').keys() | 257 branches = self.list_dir('branches').keys() |
199 branch_info = {} | 258 branch_info = {} |
200 head=self.HEAD | 259 head=self.HEAD |
201 for b in branches: | 260 for b in branches: |
214 | 273 |
215 def tags(self): | 274 def tags(self): |
216 """Get the current tags in this repo assuming a standard layout. | 275 """Get the current tags in this repo assuming a standard layout. |
217 | 276 |
218 This returns a dictionary of tag: (source path, source rev) | 277 This returns a dictionary of tag: (source path, source rev) |
278 | |
279 This method should be eliminated; this class does not have | |
280 sufficient knowledge to yield all known tags. | |
219 """ | 281 """ |
220 return self.tags_at_rev(self.HEAD) | 282 return self.tags_at_rev(self.HEAD) |
221 tags = property(tags) | 283 tags = property(tags) |
222 | 284 |
223 def tags_at_rev(self, revision): | 285 def tags_at_rev(self, revision): |
286 """Get the tags in this repo at the given revision, assuming a | |
287 standard layout. | |
288 | |
289 This returns a dictionary of tag: (source path, source rev) | |
290 | |
291 This method should be eliminated; this class does not have | |
292 sufficient knowledge to yield all known tags. | |
293 """ | |
224 try: | 294 try: |
225 tags = self.list_dir('tags', revision=revision).keys() | 295 tags = self.list_dir('tags', revision=revision).keys() |
226 except core.SubversionException, e: | 296 except core.SubversionException, e: |
227 if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: | 297 if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: |
228 return {} | 298 return {} |
273 revision = self.HEAD | 343 revision = self.HEAD |
274 r = ra.get_dir2(self.ra, dir, revision, core.SVN_DIRENT_KIND, self.pool) | 344 r = ra.get_dir2(self.ra, dir, revision, core.SVN_DIRENT_KIND, self.pool) |
275 folders, props, junk = r | 345 folders, props, junk = r |
276 return folders | 346 return folders |
277 | 347 |
278 def revisions(self, start=None, chunk_size=1000): | 348 def revisions(self, start=None, stop=None, chunk_size=_chunk_size): |
279 """Load the history of this repo. | 349 """Load the history of this repo. |
280 | 350 |
281 This is LAZY. It returns a generator, and fetches a small number | 351 This is LAZY. It returns a generator, and fetches a small number |
282 of revisions at a time. | 352 of revisions at a time. |
283 | 353 |
286 """ | 356 """ |
287 return self.fetch_history_at_paths([''], start=start, | 357 return self.fetch_history_at_paths([''], start=start, |
288 chunk_size=chunk_size) | 358 chunk_size=chunk_size) |
289 | 359 |
290 def fetch_history_at_paths(self, paths, start=None, stop=None, | 360 def fetch_history_at_paths(self, paths, start=None, stop=None, |
291 chunk_size=1000): | 361 chunk_size=_chunk_size): |
292 revisions = [] | 362 '''TODO: This method should be merged with self.revisions() as |
293 def callback(paths, revnum, author, date, message, pool): | 363 they are now functionally equivalent.''' |
294 r = Revision(revnum, author, message, date, paths, | |
295 strip_path=self.subdir) | |
296 revisions.append(r) | |
297 if not start: | 364 if not start: |
298 start = self.START | 365 start = self.START |
299 if not stop: | 366 if not stop: |
300 stop = self.HEAD | 367 stop = self.HEAD |
301 while stop > start: | 368 while stop > start: |
302 ra.get_log(self.ra, | 369 def callback(paths, revnum, author, date, message, pool): |
303 paths, | 370 r = Revision(revnum, author, message, date, paths, |
304 start+1, | 371 strip_path=self.subdir) |
305 stop, | 372 revisions.append(r) |
306 chunk_size, #limit of how many log messages to load | 373 # use a queue; we only access revisions in a FIFO manner |
307 True, # don't need to know changed paths | 374 revisions = collections.deque() |
308 True, # stop on copies | 375 |
309 callback, | 376 try: |
310 self.pool) | 377 # TODO: using min(start + chunk_size, stop) may be preferable; |
311 if len(revisions) < chunk_size: | 378 # ra.get_log(), even with chunk_size set, takes a while |
312 # this means there was no history for the path, so force the | 379 # when converting the 65k+ rev. in LLVM. |
313 # loop to exit | 380 ra.get_log(self.ra, |
314 start = stop | 381 paths, |
382 start+1, | |
383 stop, | |
384 chunk_size, #limit of how many log messages to load | |
385 True, # don't need to know changed paths | |
386 True, # stop on copies | |
387 callback, | |
388 self.pool) | |
389 except core.SubversionException, e: | |
390 if e.apr_err not in [core.SVN_ERR_FS_NOT_FOUND]: | |
391 raise | |
392 else: | |
393 raise hgutil.Abort('%s not found at revision %d!' | |
394 % (self.subdir.rstrip('/'), stop)) | |
395 | |
396 while len(revisions) > 1: | |
397 yield revisions.popleft() | |
398 | |
399 if len(revisions) == 0: | |
400 # exit the loop; there is no history for the path. | |
401 break | |
315 else: | 402 else: |
316 start = revisions[-1].revnum | 403 r = revisions.popleft() |
317 while len(revisions) > 0: | 404 start = r.revnum |
318 yield revisions[0] | 405 yield r |
319 revisions.pop(0) | 406 self.init_ra_and_client() |
320 | 407 |
321 def commit(self, paths, message, file_data, base_revision, addeddirs, | 408 def commit(self, paths, message, file_data, base_revision, addeddirs, |
322 deleteddirs, properties, copies): | 409 deleteddirs, properties, copies): |
323 """Commits the appropriate targets from revision in editor's store. | 410 """Commits the appropriate targets from revision in editor's store. |
324 """ | 411 """ |