comparison svnwrap/svn_swig_wrapper.py @ 0:f2636cfed115

Initial import of hgsubversion into a public repository.
author Augie Fackler <durin42@gmail.com>
date Tue, 30 Sep 2008 11:42:52 -0500
parents
children 1a5bb173170b
comparison
equal deleted inserted replaced
-1:000000000000 0:f2636cfed115
1 import cStringIO
2 import getpass
3 import os
4 import pwd
5 import shutil
6 import sys
7 import tempfile
8
9 from svn import client
10 from svn import core
11 from svn import delta
12 from svn import ra
13
14 svn_config = core.svn_config_get_config(None)
15
16
17 def user_pass_prompt(realm, default_username, ms, pool):
18 creds = core.svn_auth_cred_simple_t()
19 creds.may_save = ms
20 if default_username:
21 sys.stderr.write('Auth realm: %s\n' % (realm,))
22 creds.username = default_username
23 else:
24 sys.stderr.write('Auth realm: %s\n' % (realm,))
25 sys.stderr.write('Username: ')
26 sys.stderr.flush()
27 creds.username = sys.stdin.readline().strip()
28 creds.password = getpass.getpass('Password for %s: ' % creds.username)
29 return creds
30
31 def _create_auth_baton(pool):
32 """Create a Subversion authentication baton. """
33 # Give the client context baton a suite of authentication
34 # providers.h
35 providers = [
36 client.get_simple_provider(),
37 client.get_username_provider(),
38 client.get_ssl_client_cert_file_provider(),
39 client.get_ssl_client_cert_pw_file_provider(),
40 client.get_ssl_server_trust_file_provider(),
41 ]
42 # Platform-dependant authentication methods
43 if hasattr(client, 'get_windows_simple_provider'):
44 providers.append(client.get_windows_simple_provider())
45 if hasattr(client, 'get_keychain_simple_provider'):
46 providers.append(client.get_keychain_simple_provider())
47 providers.extend([client.get_simple_prompt_provider(user_pass_prompt, 2),
48 ])
49 return core.svn_auth_open(providers, pool)
50
51
52 class Revision(object):
53 """Wrapper for a Subversion revision.
54 """
55 def __init__(self, revnum, author, message, date, paths, strip_path=''):
56 self.revnum, self.author, self.message = revnum, author, message
57 # TODO parse this into a datetime
58 self.date = date
59 self.paths = {}
60 for p in paths:
61 self.paths[p[len(strip_path):]] = paths[p]
62
63 def __str__(self):
64 return 'r%d by %s' % (self.revnum, self.author)
65
66 class SubversionRepo(object):
67 """Wrapper for a Subversion repository.
68
69 This uses the SWIG Python bindings, and will only work on svn >= 1.4.
70 It takes a required param, the URL.
71 """
72 def __init__(self, url=''):
73 self.svn_url = url
74 self.auth_baton_pool = core.Pool()
75 self.auth_baton = _create_auth_baton(self.auth_baton_pool)
76
77 self.init_ra_and_client()
78 self.uuid = ra.get_uuid(self.ra, self.pool)
79 repo_root = ra.get_repos_root(self.ra, self.pool)
80 # *will* have a leading '/', would not if we used get_repos_root2
81 self.subdir = url[len(repo_root):]
82 if not self.subdir or self.subdir[-1] != '/':
83 self.subdir += '/'
84
85 def init_ra_and_client(self):
86 """Initializes the RA and client layers, because sometimes getting
87 unified diffs runs the remote server out of open files.
88 """
89 # while we're in here we'll recreate our pool
90 self.pool = core.Pool()
91 self.client_context = client.create_context()
92 self.uname = str(pwd.getpwuid(os.getuid())[0])
93 core.svn_auth_set_parameter(self.auth_baton,
94 core.SVN_AUTH_PARAM_DEFAULT_USERNAME,
95 self.uname)
96
97 self.client_context.auth_baton = self.auth_baton
98 self.client_context.config = svn_config
99 self.ra = client.open_ra_session(self.svn_url.encode('utf8'),
100 self.client_context)
101
102
103 @property
104 def HEAD(self):
105 return ra.get_latest_revnum(self.ra, self.pool)
106
107 @property
108 def START(self):
109 return 0
110
111 @property
112 def branches(self):
113 """Get the branches defined in this repo assuming a standard layout.
114 """
115 branches = self.list_dir('branches').keys()
116 branch_info = {}
117 head=self.HEAD
118 for b in branches:
119 b_path = 'branches/%s' %b
120 hist_gen = self.fetch_history_at_paths([b_path], stop=head)
121 hist = hist_gen.next()
122 source, source_rev = self._get_copy_source(b_path, cached_head=head)
123 # This if statement guards against projects that have non-ancestral
124 # branches by not listing them has branches
125 # Note that they probably are really ancestrally related, but there
126 # is just no way for us to know how.
127 if source is not None and source_rev is not None:
128 branch_info[b] = (source, source_rev, hist.revnum)
129 return branch_info
130
131 @property
132 def tags(self):
133 """Get the current tags in this repo assuming a standard layout.
134
135 This returns a dictionary of tag: (source path, source rev)
136 """
137 tags = self.list_dir('tags').keys()
138 tag_info = {}
139 head = self.HEAD
140 for t in tags:
141 tag_info[t] = self._get_copy_source('tags/%s' % t,
142 cached_head=head)
143 return tag_info
144
145 def _get_copy_source(self, path, cached_head=None):
146 """Get copy revision for the given path, assuming it was meant to be
147 a copy of the entire tree.
148 """
149 if not cached_head:
150 cached_head = self.HEAD
151 hist_gen = self.fetch_history_at_paths([path], stop=cached_head)
152 hist = hist_gen.next()
153 if hist.paths[path].copyfrom_path is None:
154 return None, None
155 source = hist.paths[path].copyfrom_path
156 source_rev = 0
157 for p in hist.paths:
158 if hist.paths[p].copyfrom_rev:
159 # We assume that the revision of the source tree as it was
160 # copied was actually the revision of the highest revision
161 # copied item. This could be wrong, but in practice it will
162 # *probably* be correct
163 if source_rev < hist.paths[p].copyfrom_rev:
164 source_rev = hist.paths[p].copyfrom_rev
165 source = source[len(self.subdir):]
166 return source, source_rev
167
168 def list_dir(self, dir, revision=None):
169 """List the contents of a server-side directory.
170
171 Returns a dict-like object with one dict key per directory entry.
172
173 Args:
174 dir: the directory to list, no leading slash
175 rev: the revision at which to list the directory, defaults to HEAD
176 """
177 if dir[-1] == '/':
178 dir = dir[:-1]
179 if revision is None:
180 revision = self.HEAD
181 r = ra.get_dir2(self.ra, dir, revision, core.SVN_DIRENT_KIND, self.pool)
182 folders, props, junk = r
183 return folders
184
185 def revisions(self, start=None, chunk_size=1000):
186 """Load the history of this repo.
187
188 This is LAZY. It returns a generator, and fetches a small number
189 of revisions at a time.
190
191 The reason this is lazy is so that you can use the same repo object
192 to perform RA calls to get deltas.
193 """
194 # NB: you'd think this would work, but you'd be wrong. I'm pretty
195 # convinced there must be some kind of svn bug here.
196 #return self.fetch_history_at_paths(['tags', 'trunk', 'branches'],
197 # start=start)
198 # this does the same thing, but at the repo root + filtering. It's
199 # kind of tough cookies, sadly.
200 for r in self.fetch_history_at_paths([''], start=start,
201 chunk_size=chunk_size):
202 should_yield = False
203 i = 0
204 paths = list(r.paths.keys())
205 while i < len(paths) and not should_yield:
206 p = paths[i]
207 if (p.startswith('trunk') or p.startswith('tags')
208 or p.startswith('branches')):
209 should_yield = True
210 i += 1
211 if should_yield:
212 yield r
213
214
215 def fetch_history_at_paths(self, paths, start=None, stop=None,
216 chunk_size=1000):
217 revisions = []
218 def callback(paths, revnum, author, date, message, pool):
219 r = Revision(revnum, author, message, date, paths,
220 strip_path=self.subdir)
221 revisions.append(r)
222 if not start:
223 start = self.START
224 if not stop:
225 stop = self.HEAD
226 while stop > start:
227 ra.get_log(self.ra, paths,
228 start+1,
229 stop,
230 chunk_size, #limit of how many log messages to load
231 True, # don't need to know changed paths
232 True, # stop on copies
233 callback,
234 self.pool)
235 if len(revisions) < chunk_size:
236 # this means there was no history for the path, so force the
237 # loop to exit
238 start = stop
239 else:
240 start = revisions[-1].revnum
241 while len(revisions) > 0:
242 yield revisions[0]
243 revisions.pop(0)
244
245 def commit(self, paths, message, file_data, base_revision, dirs):
246 """Commits the appropriate targets from revision in editor's store.
247 """
248 self.init_ra_and_client()
249 commit_info = []
250 def commit_cb(_commit_info, pool):
251 commit_info.append(_commit_info)
252 editor, edit_baton = ra.get_commit_editor2(self.ra,
253 message,
254 commit_cb,
255 None,
256 False)
257 checksum = []
258 def driver_cb(parent, path, pool):
259 if path in dirs:
260 return baton
261 base_text, new_text, action = file_data[path]
262 compute_delta = True
263 if action == 'modify':
264 baton = editor.open_file(path, parent, base_revision, pool)
265 elif action == 'add':
266 try:
267 baton = editor.add_file(path, parent, None, -1, pool)
268 except (core.SubversionException, TypeError), e:
269 print e.message
270 raise
271 elif action == 'delete':
272 baton = editor.delete_entry(path, base_revision, parent, pool)
273 compute_delta = False
274
275 if compute_delta:
276 handler, wh_baton = editor.apply_textdelta(baton, None,
277 self.pool)
278
279 txdelta_stream = delta.svn_txdelta(
280 cStringIO.StringIO(base_text), cStringIO.StringIO(new_text),
281 self.pool)
282 delta.svn_txdelta_send_txstream(txdelta_stream, handler,
283 wh_baton, pool)
284
285 delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb,
286 self.pool)
287 editor.close_edit(edit_baton, self.pool)
288
289 def get_replay(self, revision, editor, oldest_rev_i_have=0):
290 # this method has a tendency to chew through RAM if you don't re-init
291 self.init_ra_and_client()
292 e_ptr, e_baton = delta.make_editor(editor)
293 try:
294 ra.replay(self.ra, revision, oldest_rev_i_have, True, e_ptr,
295 e_baton, self.pool)
296 except core.SubversionException, e:
297 # can I depend on this number being constant?
298 if (e.message == "Server doesn't support the replay command"
299 or e.apr_err == 170003):
300 raise SubversionRepoCanNotReplay, ('This Subversion server '
301 'is older than 1.4.0, and cannot satisfy replay requests.')
302 else:
303 raise
304
305 def get_unified_diff(self, path, revision, other_path=None, other_rev=None,
306 deleted=True, ignore_type=False):
307 """Gets a unidiff of path at revision against revision-1.
308 """
309 # works around an svn server keeping too many open files (observed
310 # in an svnserve from the 1.2 era)
311 self.init_ra_and_client()
312
313 old_cwd = os.getcwd()
314 assert path[0] != '/'
315 url = self.svn_url + '/' + path
316 url2 = url
317 if other_path is not None:
318 url2 = self.svn_url + '/' + other_path
319 if other_rev is None:
320 other_rev = revision - 1
321 tmpdir = tempfile.mkdtemp('svnwrap_temp')
322 # hot tip: the swig bridge doesn't like StringIO for these bad boys
323 out_path = os.path.join(tmpdir, 'diffout')
324 error_path = os.path.join(tmpdir, 'differr')
325 out = open(out_path, 'w')
326 err = open(error_path, 'w')
327 rev_old = core.svn_opt_revision_t()
328 rev_old.kind = core.svn_opt_revision_number
329 rev_old.value.number = other_rev
330 rev_new = core.svn_opt_revision_t()
331 rev_new.kind = core.svn_opt_revision_number
332 rev_new.value.number = revision
333 client.diff3([], url2, rev_old, url, rev_new, True, True,
334 deleted, ignore_type, 'UTF-8', out, err,
335 self.client_context, self.pool)
336 out.close()
337 err.close()
338 assert len(open(error_path).read()) == 0
339 diff = open(out_path).read()
340 os.chdir(old_cwd)
341 shutil.rmtree(tmpdir)
342 return diff
343
344 def get_file(self, path, revision):
345 out = cStringIO.StringIO()
346 tmpdir = tempfile.mkdtemp('svnwrap_temp')
347 # hot tip: the swig bridge doesn't like StringIO for these bad boys
348 out_path = os.path.join(tmpdir, 'diffout')
349 out = open(out_path, 'w')
350 ra.get_file(self.ra, path,revision, out , None)
351 out.close()
352 x = open(out_path).read()
353 shutil.rmtree(tmpdir)
354 return x
355
356 def proplist(self, path, revision, recurse=False):
357 rev = core.svn_opt_revision_t()
358 rev.kind = core.svn_opt_revision_number
359 rev.value.number = revision
360 if path[-1] == '/':
361 path = path[:-1]
362 if path[0] == '/':
363 path = path[1:]
364 pl = dict(client.proplist2(self.svn_url+'/'+path, rev, rev, True,
365 self.client_context, self.pool))
366 pl2 = {}
367 for key, value in pl.iteritems():
368 pl2[key[len(self.svn_url)+1:]] = value
369 return pl2
370
371 def fetch_all_files_to_dir(self, path, revision, checkout_path):
372 rev = core.svn_opt_revision_t()
373 rev.kind = core.svn_opt_revision_number
374 rev.value.number = revision
375 client.export3(self.svn_url+'/'+path, checkout_path, rev,
376 rev, True, True, True, 'LF', # should be 'CRLF' on win32
377 self.client_context, self.pool)
378
379 class SubversionRepoCanNotReplay(Exception):
380 """Exception raised when the svn server is too old to have replay.
381 """