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