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 """