##// END OF EJS Templates
exception-handling: better handling of remote exception and logging....
marcink -
r171:c608ea73 default
parent child Browse files
Show More
@@ -1,82 +1,98 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 import sys
19 import traceback
18 20 import logging
19 21 import urlparse
20 22
21 23 log = logging.getLogger(__name__)
22 24
23 25
24 26 class RepoFactory(object):
25 27 """
26 28 Utility to create instances of repository
27 29
28 30 It provides internal caching of the `repo` object based on
29 31 the :term:`call context`.
30 32 """
31 33
32 34 def __init__(self, repo_cache):
33 35 self._cache = repo_cache
34 36
35 37 def _create_config(self, path, config):
36 38 config = {}
37 39 return config
38 40
39 41 def _create_repo(self, wire, create):
40 42 raise NotImplementedError()
41 43
42 44 def repo(self, wire, create=False):
43 45 """
44 46 Get a repository instance for the given path.
45 47
46 48 Uses internally the low level beaker API since the decorators introduce
47 49 significant overhead.
48 50 """
49 51 def create_new_repo():
50 52 return self._create_repo(wire, create)
51 53
52 54 return self._repo(wire, create_new_repo)
53 55
54 56 def _repo(self, wire, createfunc):
55 57 context = wire.get('context', None)
56 58 cache = wire.get('cache', True)
57 59
58 60 if context and cache:
59 61 cache_key = (context, wire['path'])
60 62 log.debug(
61 63 'FETCH %s@%s repo object from cache. Context: %s',
62 64 self.__class__.__name__, wire['path'], context)
63 65 return self._cache.get(key=cache_key, createfunc=createfunc)
64 66 else:
65 67 log.debug(
66 68 'INIT %s@%s repo object based on wire %s. Context: %s',
67 69 self.__class__.__name__, wire['path'], wire, context)
68 70 return createfunc()
69 71
70 72
71 73 def obfuscate_qs(query_string):
72 74 if query_string is None:
73 75 return None
74 76
75 77 parsed = []
76 78 for k, v in urlparse.parse_qsl(query_string, keep_blank_values=True):
77 79 if k in ['auth_token', 'api_key']:
78 80 v = "*****"
79 81 parsed.append((k, v))
80 82
81 83 return '&'.join('{}{}'.format(
82 84 k, '={}'.format(v) if v else '') for k, v in parsed)
85
86
87 def raise_from_original(new_type):
88 """
89 Raise a new exception type with original args and traceback.
90 """
91 exc_type, exc_value, exc_traceback = sys.exc_info()
92
93 traceback.format_exception(exc_type, exc_value, exc_traceback)
94
95 try:
96 raise new_type(*exc_value.args), None, exc_traceback
97 finally:
98 del exc_traceback
@@ -1,581 +1,586 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import logging
19 19 import os
20 20 import posixpath as vcspath
21 21 import re
22 22 import stat
23 23 import urllib
24 24 import urllib2
25 25 from functools import wraps
26 26
27 27 from dulwich import index, objects
28 28 from dulwich.client import HttpGitClient, LocalGitClient
29 29 from dulwich.errors import (
30 30 NotGitRepository, ChecksumMismatch, WrongObjectException,
31 31 MissingCommitError, ObjectMissing, HangupException,
32 32 UnexpectedCommandError)
33 33 from dulwich.repo import Repo as DulwichRepo, Tag
34 34 from dulwich.server import update_server_info
35 35
36 36 from vcsserver import exceptions, settings, subprocessio
37 37 from vcsserver.utils import safe_str
38 from vcsserver.base import RepoFactory, obfuscate_qs
38 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
39 39 from vcsserver.hgcompat import (
40 40 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
41 41
42 42
43 43 DIR_STAT = stat.S_IFDIR
44 44 FILE_MODE = stat.S_IFMT
45 45 GIT_LINK = objects.S_IFGITLINK
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 def reraise_safe_exceptions(func):
51 51 """Converts Dulwich exceptions to something neutral."""
52 52 @wraps(func)
53 53 def wrapper(*args, **kwargs):
54 54 try:
55 55 return func(*args, **kwargs)
56 56 except (ChecksumMismatch, WrongObjectException, MissingCommitError,
57 57 ObjectMissing) as e:
58 58 raise exceptions.LookupException(e.message)
59 59 except (HangupException, UnexpectedCommandError) as e:
60 60 raise exceptions.VcsException(e.message)
61 except Exception as e:
62 if not hasattr(e, '_vcs_kind'):
63 log.exception("Unhandled exception in git remote call")
64 raise_from_original(exceptions.UnhandledException)
65 raise
61 66 return wrapper
62 67
63 68
64 69 class Repo(DulwichRepo):
65 70 """
66 71 A wrapper for dulwich Repo class.
67 72
68 73 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
69 74 "Too many open files" error. We need to close all opened file descriptors
70 75 once the repo object is destroyed.
71 76
72 77 TODO: mikhail: please check if we need this wrapper after updating dulwich
73 78 to 0.12.0 +
74 79 """
75 80 def __del__(self):
76 81 if hasattr(self, 'object_store'):
77 82 self.close()
78 83
79 84
80 85 class GitFactory(RepoFactory):
81 86
82 87 def _create_repo(self, wire, create):
83 88 repo_path = str_to_dulwich(wire['path'])
84 89 return Repo(repo_path)
85 90
86 91
87 92 class GitRemote(object):
88 93
89 94 def __init__(self, factory):
90 95 self._factory = factory
91 96
92 97 self._bulk_methods = {
93 98 "author": self.commit_attribute,
94 99 "date": self.get_object_attrs,
95 100 "message": self.commit_attribute,
96 101 "parents": self.commit_attribute,
97 102 "_commit": self.revision,
98 103 }
99 104
100 105 def _assign_ref(self, wire, ref, commit_id):
101 106 repo = self._factory.repo(wire)
102 107 repo[ref] = commit_id
103 108
104 109 @reraise_safe_exceptions
105 110 def add_object(self, wire, content):
106 111 repo = self._factory.repo(wire)
107 112 blob = objects.Blob()
108 113 blob.set_raw_string(content)
109 114 repo.object_store.add_object(blob)
110 115 return blob.id
111 116
112 117 @reraise_safe_exceptions
113 118 def assert_correct_path(self, wire):
114 119 try:
115 120 self._factory.repo(wire)
116 121 except NotGitRepository as e:
117 122 # Exception can contain unicode which we convert
118 123 raise exceptions.AbortException(repr(e))
119 124
120 125 @reraise_safe_exceptions
121 126 def bare(self, wire):
122 127 repo = self._factory.repo(wire)
123 128 return repo.bare
124 129
125 130 @reraise_safe_exceptions
126 131 def blob_as_pretty_string(self, wire, sha):
127 132 repo = self._factory.repo(wire)
128 133 return repo[sha].as_pretty_string()
129 134
130 135 @reraise_safe_exceptions
131 136 def blob_raw_length(self, wire, sha):
132 137 repo = self._factory.repo(wire)
133 138 blob = repo[sha]
134 139 return blob.raw_length()
135 140
136 141 @reraise_safe_exceptions
137 142 def bulk_request(self, wire, rev, pre_load):
138 143 result = {}
139 144 for attr in pre_load:
140 145 try:
141 146 method = self._bulk_methods[attr]
142 147 args = [wire, rev]
143 148 if attr == "date":
144 149 args.extend(["commit_time", "commit_timezone"])
145 150 elif attr in ["author", "message", "parents"]:
146 151 args.append(attr)
147 152 result[attr] = method(*args)
148 153 except KeyError:
149 154 raise exceptions.VcsException(
150 155 "Unknown bulk attribute: %s" % attr)
151 156 return result
152 157
153 158 def _build_opener(self, url):
154 159 handlers = []
155 160 url_obj = url_parser(url)
156 161 _, authinfo = url_obj.authinfo()
157 162
158 163 if authinfo:
159 164 # create a password manager
160 165 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
161 166 passmgr.add_password(*authinfo)
162 167
163 168 handlers.extend((httpbasicauthhandler(passmgr),
164 169 httpdigestauthhandler(passmgr)))
165 170
166 171 return urllib2.build_opener(*handlers)
167 172
168 173 @reraise_safe_exceptions
169 174 def check_url(self, url, config):
170 175 url_obj = url_parser(url)
171 176 test_uri, _ = url_obj.authinfo()
172 177 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
173 178 url_obj.query = obfuscate_qs(url_obj.query)
174 179 cleaned_uri = str(url_obj)
175 180 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
176 181
177 182 if not test_uri.endswith('info/refs'):
178 183 test_uri = test_uri.rstrip('/') + '/info/refs'
179 184
180 185 o = self._build_opener(url)
181 186 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
182 187
183 188 q = {"service": 'git-upload-pack'}
184 189 qs = '?%s' % urllib.urlencode(q)
185 190 cu = "%s%s" % (test_uri, qs)
186 191 req = urllib2.Request(cu, None, {})
187 192
188 193 try:
189 194 log.debug("Trying to open URL %s", cleaned_uri)
190 195 resp = o.open(req)
191 196 if resp.code != 200:
192 197 raise exceptions.URLError('Return Code is not 200')
193 198 except Exception as e:
194 199 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
195 200 # means it cannot be cloned
196 201 raise exceptions.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
197 202
198 203 # now detect if it's proper git repo
199 204 gitdata = resp.read()
200 205 if 'service=git-upload-pack' in gitdata:
201 206 pass
202 207 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
203 208 # old style git can return some other format !
204 209 pass
205 210 else:
206 211 raise exceptions.URLError(
207 212 "url [%s] does not look like an git" % (cleaned_uri,))
208 213
209 214 return True
210 215
211 216 @reraise_safe_exceptions
212 217 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
213 218 remote_refs = self.fetch(wire, url, apply_refs=False)
214 219 repo = self._factory.repo(wire)
215 220 if isinstance(valid_refs, list):
216 221 valid_refs = tuple(valid_refs)
217 222
218 223 for k in remote_refs:
219 224 # only parse heads/tags and skip so called deferred tags
220 225 if k.startswith(valid_refs) and not k.endswith(deferred):
221 226 repo[k] = remote_refs[k]
222 227
223 228 if update_after_clone:
224 229 # we want to checkout HEAD
225 230 repo["HEAD"] = remote_refs["HEAD"]
226 231 index.build_index_from_tree(repo.path, repo.index_path(),
227 232 repo.object_store, repo["HEAD"].tree)
228 233
229 234 # TODO: this is quite complex, check if that can be simplified
230 235 @reraise_safe_exceptions
231 236 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
232 237 repo = self._factory.repo(wire)
233 238 object_store = repo.object_store
234 239
235 240 # Create tree and populates it with blobs
236 241 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
237 242
238 243 for node in updated:
239 244 # Compute subdirs if needed
240 245 dirpath, nodename = vcspath.split(node['path'])
241 246 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
242 247 parent = commit_tree
243 248 ancestors = [('', parent)]
244 249
245 250 # Tries to dig for the deepest existing tree
246 251 while dirnames:
247 252 curdir = dirnames.pop(0)
248 253 try:
249 254 dir_id = parent[curdir][1]
250 255 except KeyError:
251 256 # put curdir back into dirnames and stops
252 257 dirnames.insert(0, curdir)
253 258 break
254 259 else:
255 260 # If found, updates parent
256 261 parent = repo[dir_id]
257 262 ancestors.append((curdir, parent))
258 263 # Now parent is deepest existing tree and we need to create
259 264 # subtrees for dirnames (in reverse order)
260 265 # [this only applies for nodes from added]
261 266 new_trees = []
262 267
263 268 blob = objects.Blob.from_string(node['content'])
264 269
265 270 if dirnames:
266 271 # If there are trees which should be created we need to build
267 272 # them now (in reverse order)
268 273 reversed_dirnames = list(reversed(dirnames))
269 274 curtree = objects.Tree()
270 275 curtree[node['node_path']] = node['mode'], blob.id
271 276 new_trees.append(curtree)
272 277 for dirname in reversed_dirnames[:-1]:
273 278 newtree = objects.Tree()
274 279 newtree[dirname] = (DIR_STAT, curtree.id)
275 280 new_trees.append(newtree)
276 281 curtree = newtree
277 282 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
278 283 else:
279 284 parent.add(
280 285 name=node['node_path'], mode=node['mode'], hexsha=blob.id)
281 286
282 287 new_trees.append(parent)
283 288 # Update ancestors
284 289 reversed_ancestors = reversed(
285 290 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
286 291 for parent, tree, path in reversed_ancestors:
287 292 parent[path] = (DIR_STAT, tree.id)
288 293 object_store.add_object(tree)
289 294
290 295 object_store.add_object(blob)
291 296 for tree in new_trees:
292 297 object_store.add_object(tree)
293 298
294 299 for node_path in removed:
295 300 paths = node_path.split('/')
296 301 tree = commit_tree
297 302 trees = [tree]
298 303 # Traverse deep into the forest...
299 304 for path in paths:
300 305 try:
301 306 obj = repo[tree[path][1]]
302 307 if isinstance(obj, objects.Tree):
303 308 trees.append(obj)
304 309 tree = obj
305 310 except KeyError:
306 311 break
307 312 # Cut down the blob and all rotten trees on the way back...
308 313 for path, tree in reversed(zip(paths, trees)):
309 314 del tree[path]
310 315 if tree:
311 316 # This tree still has elements - don't remove it or any
312 317 # of it's parents
313 318 break
314 319
315 320 object_store.add_object(commit_tree)
316 321
317 322 # Create commit
318 323 commit = objects.Commit()
319 324 commit.tree = commit_tree.id
320 325 for k, v in commit_data.iteritems():
321 326 setattr(commit, k, v)
322 327 object_store.add_object(commit)
323 328
324 329 ref = 'refs/heads/%s' % branch
325 330 repo.refs[ref] = commit.id
326 331
327 332 return commit.id
328 333
329 334 @reraise_safe_exceptions
330 335 def fetch(self, wire, url, apply_refs=True, refs=None):
331 336 if url != 'default' and '://' not in url:
332 337 client = LocalGitClient(url)
333 338 else:
334 339 url_obj = url_parser(url)
335 340 o = self._build_opener(url)
336 341 url, _ = url_obj.authinfo()
337 342 client = HttpGitClient(base_url=url, opener=o)
338 343 repo = self._factory.repo(wire)
339 344
340 345 determine_wants = repo.object_store.determine_wants_all
341 346 if refs:
342 347 def determine_wants_requested(references):
343 348 return [references[r] for r in references if r in refs]
344 349 determine_wants = determine_wants_requested
345 350
346 351 try:
347 352 remote_refs = client.fetch(
348 353 path=url, target=repo, determine_wants=determine_wants)
349 354 except NotGitRepository as e:
350 355 log.warning(
351 356 'Trying to fetch from "%s" failed, not a Git repository.', url)
352 357 # Exception can contain unicode which we convert
353 358 raise exceptions.AbortException(repr(e))
354 359
355 360 # mikhail: client.fetch() returns all the remote refs, but fetches only
356 361 # refs filtered by `determine_wants` function. We need to filter result
357 362 # as well
358 363 if refs:
359 364 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
360 365
361 366 if apply_refs:
362 367 # TODO: johbo: Needs proper test coverage with a git repository
363 368 # that contains a tag object, so that we would end up with
364 369 # a peeled ref at this point.
365 370 PEELED_REF_MARKER = '^{}'
366 371 for k in remote_refs:
367 372 if k.endswith(PEELED_REF_MARKER):
368 373 log.info("Skipping peeled reference %s", k)
369 374 continue
370 375 repo[k] = remote_refs[k]
371 376
372 377 if refs:
373 378 # mikhail: explicitly set the head to the last ref.
374 379 repo['HEAD'] = remote_refs[refs[-1]]
375 380
376 381 # TODO: mikhail: should we return remote_refs here to be
377 382 # consistent?
378 383 else:
379 384 return remote_refs
380 385
381 386 @reraise_safe_exceptions
382 387 def get_remote_refs(self, wire, url):
383 388 repo = Repo(url)
384 389 return repo.get_refs()
385 390
386 391 @reraise_safe_exceptions
387 392 def get_description(self, wire):
388 393 repo = self._factory.repo(wire)
389 394 return repo.get_description()
390 395
391 396 @reraise_safe_exceptions
392 397 def get_file_history(self, wire, file_path, commit_id, limit):
393 398 repo = self._factory.repo(wire)
394 399 include = [commit_id]
395 400 paths = [file_path]
396 401
397 402 walker = repo.get_walker(include, paths=paths, max_entries=limit)
398 403 return [x.commit.id for x in walker]
399 404
400 405 @reraise_safe_exceptions
401 406 def get_missing_revs(self, wire, rev1, rev2, path2):
402 407 repo = self._factory.repo(wire)
403 408 LocalGitClient(thin_packs=False).fetch(path2, repo)
404 409
405 410 wire_remote = wire.copy()
406 411 wire_remote['path'] = path2
407 412 repo_remote = self._factory.repo(wire_remote)
408 413 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
409 414
410 415 revs = [
411 416 x.commit.id
412 417 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
413 418 return revs
414 419
415 420 @reraise_safe_exceptions
416 421 def get_object(self, wire, sha):
417 422 repo = self._factory.repo(wire)
418 423 obj = repo.get_object(sha)
419 424 commit_id = obj.id
420 425
421 426 if isinstance(obj, Tag):
422 427 commit_id = obj.object[1]
423 428
424 429 return {
425 430 'id': obj.id,
426 431 'type': obj.type_name,
427 432 'commit_id': commit_id
428 433 }
429 434
430 435 @reraise_safe_exceptions
431 436 def get_object_attrs(self, wire, sha, *attrs):
432 437 repo = self._factory.repo(wire)
433 438 obj = repo.get_object(sha)
434 439 return list(getattr(obj, a) for a in attrs)
435 440
436 441 @reraise_safe_exceptions
437 442 def get_refs(self, wire):
438 443 repo = self._factory.repo(wire)
439 444 result = {}
440 445 for ref, sha in repo.refs.as_dict().items():
441 446 peeled_sha = repo.get_peeled(ref)
442 447 result[ref] = peeled_sha
443 448 return result
444 449
445 450 @reraise_safe_exceptions
446 451 def get_refs_path(self, wire):
447 452 repo = self._factory.repo(wire)
448 453 return repo.refs.path
449 454
450 455 @reraise_safe_exceptions
451 456 def head(self, wire):
452 457 repo = self._factory.repo(wire)
453 458 return repo.head()
454 459
455 460 @reraise_safe_exceptions
456 461 def init(self, wire):
457 462 repo_path = str_to_dulwich(wire['path'])
458 463 self.repo = Repo.init(repo_path)
459 464
460 465 @reraise_safe_exceptions
461 466 def init_bare(self, wire):
462 467 repo_path = str_to_dulwich(wire['path'])
463 468 self.repo = Repo.init_bare(repo_path)
464 469
465 470 @reraise_safe_exceptions
466 471 def revision(self, wire, rev):
467 472 repo = self._factory.repo(wire)
468 473 obj = repo[rev]
469 474 obj_data = {
470 475 'id': obj.id,
471 476 }
472 477 try:
473 478 obj_data['tree'] = obj.tree
474 479 except AttributeError:
475 480 pass
476 481 return obj_data
477 482
478 483 @reraise_safe_exceptions
479 484 def commit_attribute(self, wire, rev, attr):
480 485 repo = self._factory.repo(wire)
481 486 obj = repo[rev]
482 487 return getattr(obj, attr)
483 488
484 489 @reraise_safe_exceptions
485 490 def set_refs(self, wire, key, value):
486 491 repo = self._factory.repo(wire)
487 492 repo.refs[key] = value
488 493
489 494 @reraise_safe_exceptions
490 495 def remove_ref(self, wire, key):
491 496 repo = self._factory.repo(wire)
492 497 del repo.refs[key]
493 498
494 499 @reraise_safe_exceptions
495 500 def tree_changes(self, wire, source_id, target_id):
496 501 repo = self._factory.repo(wire)
497 502 source = repo[source_id].tree if source_id else None
498 503 target = repo[target_id].tree
499 504 result = repo.object_store.tree_changes(source, target)
500 505 return list(result)
501 506
502 507 @reraise_safe_exceptions
503 508 def tree_items(self, wire, tree_id):
504 509 repo = self._factory.repo(wire)
505 510 tree = repo[tree_id]
506 511
507 512 result = []
508 513 for item in tree.iteritems():
509 514 item_sha = item.sha
510 515 item_mode = item.mode
511 516
512 517 if FILE_MODE(item_mode) == GIT_LINK:
513 518 item_type = "link"
514 519 else:
515 520 item_type = repo[item_sha].type_name
516 521
517 522 result.append((item.path, item_mode, item_sha, item_type))
518 523 return result
519 524
520 525 @reraise_safe_exceptions
521 526 def update_server_info(self, wire):
522 527 repo = self._factory.repo(wire)
523 528 update_server_info(repo)
524 529
525 530 @reraise_safe_exceptions
526 531 def discover_git_version(self):
527 532 stdout, _ = self.run_git_command(
528 533 {}, ['--version'], _bare=True, _safe=True)
529 534 prefix = 'git version'
530 535 if stdout.startswith(prefix):
531 536 stdout = stdout[len(prefix):]
532 537 return stdout.strip()
533 538
534 539 @reraise_safe_exceptions
535 540 def run_git_command(self, wire, cmd, **opts):
536 541 path = wire.get('path', None)
537 542
538 543 if path and os.path.isdir(path):
539 544 opts['cwd'] = path
540 545
541 546 if '_bare' in opts:
542 547 _copts = []
543 548 del opts['_bare']
544 549 else:
545 550 _copts = ['-c', 'core.quotepath=false', ]
546 551 safe_call = False
547 552 if '_safe' in opts:
548 553 # no exc on failure
549 554 del opts['_safe']
550 555 safe_call = True
551 556
552 557 gitenv = os.environ.copy()
553 558 gitenv.update(opts.pop('extra_env', {}))
554 559 # need to clean fix GIT_DIR !
555 560 if 'GIT_DIR' in gitenv:
556 561 del gitenv['GIT_DIR']
557 562 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
558 563
559 564 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
560 565
561 566 try:
562 567 _opts = {'env': gitenv, 'shell': False}
563 568 _opts.update(opts)
564 569 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
565 570
566 571 return ''.join(p), ''.join(p.error)
567 572 except (EnvironmentError, OSError) as err:
568 573 tb_err = ("Couldn't run git command (%s).\n"
569 574 "Original error was:%s\n" % (cmd, err))
570 575 log.exception(tb_err)
571 576 if safe_call:
572 577 return '', err
573 578 else:
574 579 raise exceptions.VcsException(tb_err)
575 580
576 581
577 582 def str_to_dulwich(value):
578 583 """
579 584 Dulwich 0.10.1a requires `unicode` objects to be passed in.
580 585 """
581 586 return value.decode(settings.WIRE_ENCODING)
@@ -1,723 +1,711 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import logging
20 20 import stat
21 import sys
22 21 import urllib
23 22 import urllib2
24 23
25 24 from hgext import largefiles, rebase
26 25 from hgext.strip import strip as hgext_strip
27 26 from mercurial import commands
28 27 from mercurial import unionrepo
29 28
30 29 from vcsserver import exceptions
31 from vcsserver.base import RepoFactory, obfuscate_qs
30 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
32 31 from vcsserver.hgcompat import (
33 32 archival, bin, clone, config as hgconfig, diffopts, hex,
34 33 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
35 34 httppeer, localrepository, match, memctx, exchange, memfilectx, nullrev,
36 35 patch, peer, revrange, ui, Abort, LookupError, RepoError, RepoLookupError,
37 36 InterventionRequired, RequirementError)
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 41 def make_ui_from_config(repo_config):
43 42 baseui = ui.ui()
44 43
45 44 # clean the baseui object
46 45 baseui._ocfg = hgconfig.config()
47 46 baseui._ucfg = hgconfig.config()
48 47 baseui._tcfg = hgconfig.config()
49 48
50 49 for section, option, value in repo_config:
51 50 baseui.setconfig(section, option, value)
52 51
53 52 # make our hgweb quiet so it doesn't print output
54 53 baseui.setconfig('ui', 'quiet', 'true')
55 54
56 55 # force mercurial to only use 1 thread, otherwise it may try to set a
57 56 # signal in a non-main thread, thus generating a ValueError.
58 57 baseui.setconfig('worker', 'numcpus', 1)
59 58
60 59 # If there is no config for the largefiles extension, we explicitly disable
61 60 # it here. This overrides settings from repositories hgrc file. Recent
62 61 # mercurial versions enable largefiles in hgrc on clone from largefile
63 62 # repo.
64 63 if not baseui.hasconfig('extensions', 'largefiles'):
65 64 log.debug('Explicitly disable largefiles extension for repo.')
66 65 baseui.setconfig('extensions', 'largefiles', '!')
67 66
68 67 return baseui
69 68
70 69
71 70 def reraise_safe_exceptions(func):
72 71 """Decorator for converting mercurial exceptions to something neutral."""
73 72 def wrapper(*args, **kwargs):
74 73 try:
75 74 return func(*args, **kwargs)
76 75 except (Abort, InterventionRequired):
77 76 raise_from_original(exceptions.AbortException)
78 77 except RepoLookupError:
79 78 raise_from_original(exceptions.LookupException)
80 79 except RequirementError:
81 80 raise_from_original(exceptions.RequirementException)
82 81 except RepoError:
83 82 raise_from_original(exceptions.VcsException)
84 83 except LookupError:
85 84 raise_from_original(exceptions.LookupException)
86 85 except Exception as e:
87 86 if not hasattr(e, '_vcs_kind'):
88 87 log.exception("Unhandled exception in hg remote call")
89 88 raise_from_original(exceptions.UnhandledException)
90 89 raise
91 90 return wrapper
92 91
93 92
94 def raise_from_original(new_type):
95 """
96 Raise a new exception type with original args and traceback.
97 """
98 _, original, traceback = sys.exc_info()
99 try:
100 raise new_type(*original.args), None, traceback
101 finally:
102 del traceback
103
104
105 93 class MercurialFactory(RepoFactory):
106 94
107 95 def _create_config(self, config, hooks=True):
108 96 if not hooks:
109 97 hooks_to_clean = frozenset((
110 98 'changegroup.repo_size', 'preoutgoing.pre_pull',
111 99 'outgoing.pull_logger', 'prechangegroup.pre_push'))
112 100 new_config = []
113 101 for section, option, value in config:
114 102 if section == 'hooks' and option in hooks_to_clean:
115 103 continue
116 104 new_config.append((section, option, value))
117 105 config = new_config
118 106
119 107 baseui = make_ui_from_config(config)
120 108 return baseui
121 109
122 110 def _create_repo(self, wire, create):
123 111 baseui = self._create_config(wire["config"])
124 112 return localrepository(baseui, wire["path"], create)
125 113
126 114
127 115 class HgRemote(object):
128 116
129 117 def __init__(self, factory):
130 118 self._factory = factory
131 119
132 120 self._bulk_methods = {
133 121 "affected_files": self.ctx_files,
134 122 "author": self.ctx_user,
135 123 "branch": self.ctx_branch,
136 124 "children": self.ctx_children,
137 125 "date": self.ctx_date,
138 126 "message": self.ctx_description,
139 127 "parents": self.ctx_parents,
140 128 "status": self.ctx_status,
141 129 "_file_paths": self.ctx_list,
142 130 }
143 131
144 132 @reraise_safe_exceptions
145 133 def discover_hg_version(self):
146 134 from mercurial import util
147 135 return util.version()
148 136
149 137 @reraise_safe_exceptions
150 138 def archive_repo(self, archive_path, mtime, file_info, kind):
151 139 if kind == "tgz":
152 140 archiver = archival.tarit(archive_path, mtime, "gz")
153 141 elif kind == "tbz2":
154 142 archiver = archival.tarit(archive_path, mtime, "bz2")
155 143 elif kind == 'zip':
156 144 archiver = archival.zipit(archive_path, mtime)
157 145 else:
158 146 raise exceptions.ArchiveException(
159 147 'Remote does not support: "%s".' % kind)
160 148
161 149 for f_path, f_mode, f_is_link, f_content in file_info:
162 150 archiver.addfile(f_path, f_mode, f_is_link, f_content)
163 151 archiver.done()
164 152
165 153 @reraise_safe_exceptions
166 154 def bookmarks(self, wire):
167 155 repo = self._factory.repo(wire)
168 156 return dict(repo._bookmarks)
169 157
170 158 @reraise_safe_exceptions
171 159 def branches(self, wire, normal, closed):
172 160 repo = self._factory.repo(wire)
173 161 iter_branches = repo.branchmap().iterbranches()
174 162 bt = {}
175 163 for branch_name, _heads, tip, is_closed in iter_branches:
176 164 if normal and not is_closed:
177 165 bt[branch_name] = tip
178 166 if closed and is_closed:
179 167 bt[branch_name] = tip
180 168
181 169 return bt
182 170
183 171 @reraise_safe_exceptions
184 172 def bulk_request(self, wire, rev, pre_load):
185 173 result = {}
186 174 for attr in pre_load:
187 175 try:
188 176 method = self._bulk_methods[attr]
189 177 result[attr] = method(wire, rev)
190 178 except KeyError:
191 179 raise exceptions.VcsException(
192 180 'Unknown bulk attribute: "%s"' % attr)
193 181 return result
194 182
195 183 @reraise_safe_exceptions
196 184 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
197 185 baseui = self._factory._create_config(wire["config"], hooks=hooks)
198 186 clone(baseui, source, dest, noupdate=not update_after_clone)
199 187
200 188 @reraise_safe_exceptions
201 189 def commitctx(
202 190 self, wire, message, parents, commit_time, commit_timezone,
203 191 user, files, extra, removed, updated):
204 192
205 193 def _filectxfn(_repo, memctx, path):
206 194 """
207 195 Marks given path as added/changed/removed in a given _repo. This is
208 196 for internal mercurial commit function.
209 197 """
210 198
211 199 # check if this path is removed
212 200 if path in removed:
213 201 # returning None is a way to mark node for removal
214 202 return None
215 203
216 204 # check if this path is added
217 205 for node in updated:
218 206 if node['path'] == path:
219 207 return memfilectx(
220 208 _repo,
221 209 path=node['path'],
222 210 data=node['content'],
223 211 islink=False,
224 212 isexec=bool(node['mode'] & stat.S_IXUSR),
225 213 copied=False,
226 214 memctx=memctx)
227 215
228 216 raise exceptions.AbortException(
229 217 "Given path haven't been marked as added, "
230 218 "changed or removed (%s)" % path)
231 219
232 220 repo = self._factory.repo(wire)
233 221
234 222 commit_ctx = memctx(
235 223 repo=repo,
236 224 parents=parents,
237 225 text=message,
238 226 files=files,
239 227 filectxfn=_filectxfn,
240 228 user=user,
241 229 date=(commit_time, commit_timezone),
242 230 extra=extra)
243 231
244 232 n = repo.commitctx(commit_ctx)
245 233 new_id = hex(n)
246 234
247 235 return new_id
248 236
249 237 @reraise_safe_exceptions
250 238 def ctx_branch(self, wire, revision):
251 239 repo = self._factory.repo(wire)
252 240 ctx = repo[revision]
253 241 return ctx.branch()
254 242
255 243 @reraise_safe_exceptions
256 244 def ctx_children(self, wire, revision):
257 245 repo = self._factory.repo(wire)
258 246 ctx = repo[revision]
259 247 return [child.rev() for child in ctx.children()]
260 248
261 249 @reraise_safe_exceptions
262 250 def ctx_date(self, wire, revision):
263 251 repo = self._factory.repo(wire)
264 252 ctx = repo[revision]
265 253 return ctx.date()
266 254
267 255 @reraise_safe_exceptions
268 256 def ctx_description(self, wire, revision):
269 257 repo = self._factory.repo(wire)
270 258 ctx = repo[revision]
271 259 return ctx.description()
272 260
273 261 @reraise_safe_exceptions
274 262 def ctx_diff(
275 263 self, wire, revision, git=True, ignore_whitespace=True, context=3):
276 264 repo = self._factory.repo(wire)
277 265 ctx = repo[revision]
278 266 result = ctx.diff(
279 267 git=git, ignore_whitespace=ignore_whitespace, context=context)
280 268 return list(result)
281 269
282 270 @reraise_safe_exceptions
283 271 def ctx_files(self, wire, revision):
284 272 repo = self._factory.repo(wire)
285 273 ctx = repo[revision]
286 274 return ctx.files()
287 275
288 276 @reraise_safe_exceptions
289 277 def ctx_list(self, path, revision):
290 278 repo = self._factory.repo(path)
291 279 ctx = repo[revision]
292 280 return list(ctx)
293 281
294 282 @reraise_safe_exceptions
295 283 def ctx_parents(self, wire, revision):
296 284 repo = self._factory.repo(wire)
297 285 ctx = repo[revision]
298 286 return [parent.rev() for parent in ctx.parents()]
299 287
300 288 @reraise_safe_exceptions
301 289 def ctx_substate(self, wire, revision):
302 290 repo = self._factory.repo(wire)
303 291 ctx = repo[revision]
304 292 return ctx.substate
305 293
306 294 @reraise_safe_exceptions
307 295 def ctx_status(self, wire, revision):
308 296 repo = self._factory.repo(wire)
309 297 ctx = repo[revision]
310 298 status = repo[ctx.p1().node()].status(other=ctx.node())
311 299 # object of status (odd, custom named tuple in mercurial) is not
312 300 # correctly serializable via Pyro, we make it a list, as the underling
313 301 # API expects this to be a list
314 302 return list(status)
315 303
316 304 @reraise_safe_exceptions
317 305 def ctx_user(self, wire, revision):
318 306 repo = self._factory.repo(wire)
319 307 ctx = repo[revision]
320 308 return ctx.user()
321 309
322 310 @reraise_safe_exceptions
323 311 def check_url(self, url, config):
324 312 _proto = None
325 313 if '+' in url[:url.find('://')]:
326 314 _proto = url[0:url.find('+')]
327 315 url = url[url.find('+') + 1:]
328 316 handlers = []
329 317 url_obj = url_parser(url)
330 318 test_uri, authinfo = url_obj.authinfo()
331 319 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
332 320 url_obj.query = obfuscate_qs(url_obj.query)
333 321
334 322 cleaned_uri = str(url_obj)
335 323 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
336 324
337 325 if authinfo:
338 326 # create a password manager
339 327 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
340 328 passmgr.add_password(*authinfo)
341 329
342 330 handlers.extend((httpbasicauthhandler(passmgr),
343 331 httpdigestauthhandler(passmgr)))
344 332
345 333 o = urllib2.build_opener(*handlers)
346 334 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
347 335 ('Accept', 'application/mercurial-0.1')]
348 336
349 337 q = {"cmd": 'between'}
350 338 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
351 339 qs = '?%s' % urllib.urlencode(q)
352 340 cu = "%s%s" % (test_uri, qs)
353 341 req = urllib2.Request(cu, None, {})
354 342
355 343 try:
356 344 log.debug("Trying to open URL %s", cleaned_uri)
357 345 resp = o.open(req)
358 346 if resp.code != 200:
359 347 raise exceptions.URLError('Return Code is not 200')
360 348 except Exception as e:
361 349 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
362 350 # means it cannot be cloned
363 351 raise exceptions.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
364 352
365 353 # now check if it's a proper hg repo, but don't do it for svn
366 354 try:
367 355 if _proto == 'svn':
368 356 pass
369 357 else:
370 358 # check for pure hg repos
371 359 log.debug(
372 360 "Verifying if URL is a Mercurial repository: %s",
373 361 cleaned_uri)
374 362 httppeer(make_ui_from_config(config), url).lookup('tip')
375 363 except Exception as e:
376 364 log.warning("URL is not a valid Mercurial repository: %s",
377 365 cleaned_uri)
378 366 raise exceptions.URLError(
379 367 "url [%s] does not look like an hg repo org_exc: %s"
380 368 % (cleaned_uri, e))
381 369
382 370 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
383 371 return True
384 372
385 373 @reraise_safe_exceptions
386 374 def diff(
387 375 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
388 376 context):
389 377 repo = self._factory.repo(wire)
390 378
391 379 if file_filter:
392 380 match_filter = match(file_filter[0], '', [file_filter[1]])
393 381 else:
394 382 match_filter = file_filter
395 383 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
396 384
397 385 try:
398 386 return "".join(patch.diff(
399 387 repo, node1=rev1, node2=rev2, match=match_filter, opts=opts))
400 388 except RepoLookupError:
401 389 raise exceptions.LookupException()
402 390
403 391 @reraise_safe_exceptions
404 392 def file_history(self, wire, revision, path, limit):
405 393 repo = self._factory.repo(wire)
406 394
407 395 ctx = repo[revision]
408 396 fctx = ctx.filectx(path)
409 397
410 398 def history_iter():
411 399 limit_rev = fctx.rev()
412 400 for obj in reversed(list(fctx.filelog())):
413 401 obj = fctx.filectx(obj)
414 402 if limit_rev >= obj.rev():
415 403 yield obj
416 404
417 405 history = []
418 406 for cnt, obj in enumerate(history_iter()):
419 407 if limit and cnt >= limit:
420 408 break
421 409 history.append(hex(obj.node()))
422 410
423 411 return [x for x in history]
424 412
425 413 @reraise_safe_exceptions
426 414 def file_history_untill(self, wire, revision, path, limit):
427 415 repo = self._factory.repo(wire)
428 416 ctx = repo[revision]
429 417 fctx = ctx.filectx(path)
430 418
431 419 file_log = list(fctx.filelog())
432 420 if limit:
433 421 # Limit to the last n items
434 422 file_log = file_log[-limit:]
435 423
436 424 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
437 425
438 426 @reraise_safe_exceptions
439 427 def fctx_annotate(self, wire, revision, path):
440 428 repo = self._factory.repo(wire)
441 429 ctx = repo[revision]
442 430 fctx = ctx.filectx(path)
443 431
444 432 result = []
445 433 for i, annotate_data in enumerate(fctx.annotate()):
446 434 ln_no = i + 1
447 435 node_info, content = annotate_data
448 436 sha = hex(node_info[0].node())
449 437 result.append((ln_no, sha, content))
450 438 return result
451 439
452 440 @reraise_safe_exceptions
453 441 def fctx_data(self, wire, revision, path):
454 442 repo = self._factory.repo(wire)
455 443 ctx = repo[revision]
456 444 fctx = ctx.filectx(path)
457 445 return fctx.data()
458 446
459 447 @reraise_safe_exceptions
460 448 def fctx_flags(self, wire, revision, path):
461 449 repo = self._factory.repo(wire)
462 450 ctx = repo[revision]
463 451 fctx = ctx.filectx(path)
464 452 return fctx.flags()
465 453
466 454 @reraise_safe_exceptions
467 455 def fctx_size(self, wire, revision, path):
468 456 repo = self._factory.repo(wire)
469 457 ctx = repo[revision]
470 458 fctx = ctx.filectx(path)
471 459 return fctx.size()
472 460
473 461 @reraise_safe_exceptions
474 462 def get_all_commit_ids(self, wire, name):
475 463 repo = self._factory.repo(wire)
476 464 revs = repo.filtered(name).changelog.index
477 465 return map(lambda x: hex(x[7]), revs)[:-1]
478 466
479 467 @reraise_safe_exceptions
480 468 def get_config_value(self, wire, section, name, untrusted=False):
481 469 repo = self._factory.repo(wire)
482 470 return repo.ui.config(section, name, untrusted=untrusted)
483 471
484 472 @reraise_safe_exceptions
485 473 def get_config_bool(self, wire, section, name, untrusted=False):
486 474 repo = self._factory.repo(wire)
487 475 return repo.ui.configbool(section, name, untrusted=untrusted)
488 476
489 477 @reraise_safe_exceptions
490 478 def get_config_list(self, wire, section, name, untrusted=False):
491 479 repo = self._factory.repo(wire)
492 480 return repo.ui.configlist(section, name, untrusted=untrusted)
493 481
494 482 @reraise_safe_exceptions
495 483 def is_large_file(self, wire, path):
496 484 return largefiles.lfutil.isstandin(path)
497 485
498 486 @reraise_safe_exceptions
499 487 def in_store(self, wire, sha):
500 488 repo = self._factory.repo(wire)
501 489 return largefiles.lfutil.instore(repo, sha)
502 490
503 491 @reraise_safe_exceptions
504 492 def in_user_cache(self, wire, sha):
505 493 repo = self._factory.repo(wire)
506 494 return largefiles.lfutil.inusercache(repo.ui, sha)
507 495
508 496 @reraise_safe_exceptions
509 497 def store_path(self, wire, sha):
510 498 repo = self._factory.repo(wire)
511 499 return largefiles.lfutil.storepath(repo, sha)
512 500
513 501 @reraise_safe_exceptions
514 502 def link(self, wire, sha, path):
515 503 repo = self._factory.repo(wire)
516 504 largefiles.lfutil.link(
517 505 largefiles.lfutil.usercachepath(repo.ui, sha), path)
518 506
519 507 @reraise_safe_exceptions
520 508 def localrepository(self, wire, create=False):
521 509 self._factory.repo(wire, create=create)
522 510
523 511 @reraise_safe_exceptions
524 512 def lookup(self, wire, revision, both):
525 513 # TODO Paris: Ugly hack to "deserialize" long for msgpack
526 514 if isinstance(revision, float):
527 515 revision = long(revision)
528 516 repo = self._factory.repo(wire)
529 517 try:
530 518 ctx = repo[revision]
531 519 except RepoLookupError:
532 520 raise exceptions.LookupException(revision)
533 521 except LookupError as e:
534 522 raise exceptions.LookupException(e.name)
535 523
536 524 if not both:
537 525 return ctx.hex()
538 526
539 527 ctx = repo[ctx.hex()]
540 528 return ctx.hex(), ctx.rev()
541 529
542 530 @reraise_safe_exceptions
543 531 def pull(self, wire, url, commit_ids=None):
544 532 repo = self._factory.repo(wire)
545 533 remote = peer(repo, {}, url)
546 534 if commit_ids:
547 535 commit_ids = [bin(commit_id) for commit_id in commit_ids]
548 536
549 537 return exchange.pull(
550 538 repo, remote, heads=commit_ids, force=None).cgresult
551 539
552 540 @reraise_safe_exceptions
553 541 def revision(self, wire, rev):
554 542 repo = self._factory.repo(wire)
555 543 ctx = repo[rev]
556 544 return ctx.rev()
557 545
558 546 @reraise_safe_exceptions
559 547 def rev_range(self, wire, filter):
560 548 repo = self._factory.repo(wire)
561 549 revisions = [rev for rev in revrange(repo, filter)]
562 550 return revisions
563 551
564 552 @reraise_safe_exceptions
565 553 def rev_range_hash(self, wire, node):
566 554 repo = self._factory.repo(wire)
567 555
568 556 def get_revs(repo, rev_opt):
569 557 if rev_opt:
570 558 revs = revrange(repo, rev_opt)
571 559 if len(revs) == 0:
572 560 return (nullrev, nullrev)
573 561 return max(revs), min(revs)
574 562 else:
575 563 return len(repo) - 1, 0
576 564
577 565 stop, start = get_revs(repo, [node + ':'])
578 566 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
579 567 return revs
580 568
581 569 @reraise_safe_exceptions
582 570 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
583 571 other_path = kwargs.pop('other_path', None)
584 572
585 573 # case when we want to compare two independent repositories
586 574 if other_path and other_path != wire["path"]:
587 575 baseui = self._factory._create_config(wire["config"])
588 576 repo = unionrepo.unionrepository(baseui, other_path, wire["path"])
589 577 else:
590 578 repo = self._factory.repo(wire)
591 579 return list(repo.revs(rev_spec, *args))
592 580
593 581 @reraise_safe_exceptions
594 582 def strip(self, wire, revision, update, backup):
595 583 repo = self._factory.repo(wire)
596 584 ctx = repo[revision]
597 585 hgext_strip(
598 586 repo.baseui, repo, ctx.node(), update=update, backup=backup)
599 587
600 588 @reraise_safe_exceptions
601 589 def tag(self, wire, name, revision, message, local, user,
602 590 tag_time, tag_timezone):
603 591 repo = self._factory.repo(wire)
604 592 ctx = repo[revision]
605 593 node = ctx.node()
606 594
607 595 date = (tag_time, tag_timezone)
608 596 try:
609 597 repo.tag(name, node, message, local, user, date)
610 598 except Abort as e:
611 599 log.exception("Tag operation aborted")
612 600 # Exception can contain unicode which we convert
613 601 raise exceptions.AbortException(repr(e))
614 602
615 603 @reraise_safe_exceptions
616 604 def tags(self, wire):
617 605 repo = self._factory.repo(wire)
618 606 return repo.tags()
619 607
620 608 @reraise_safe_exceptions
621 609 def update(self, wire, node=None, clean=False):
622 610 repo = self._factory.repo(wire)
623 611 baseui = self._factory._create_config(wire['config'])
624 612 commands.update(baseui, repo, node=node, clean=clean)
625 613
626 614 @reraise_safe_exceptions
627 615 def identify(self, wire):
628 616 repo = self._factory.repo(wire)
629 617 baseui = self._factory._create_config(wire['config'])
630 618 output = io.BytesIO()
631 619 baseui.write = output.write
632 620 # This is required to get a full node id
633 621 baseui.debugflag = True
634 622 commands.identify(baseui, repo, id=True)
635 623
636 624 return output.getvalue()
637 625
638 626 @reraise_safe_exceptions
639 627 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
640 628 hooks=True):
641 629 repo = self._factory.repo(wire)
642 630 baseui = self._factory._create_config(wire['config'], hooks=hooks)
643 631
644 632 # Mercurial internally has a lot of logic that checks ONLY if
645 633 # option is defined, we just pass those if they are defined then
646 634 opts = {}
647 635 if bookmark:
648 636 opts['bookmark'] = bookmark
649 637 if branch:
650 638 opts['branch'] = branch
651 639 if revision:
652 640 opts['rev'] = revision
653 641
654 642 commands.pull(baseui, repo, source, **opts)
655 643
656 644 @reraise_safe_exceptions
657 645 def heads(self, wire, branch=None):
658 646 repo = self._factory.repo(wire)
659 647 baseui = self._factory._create_config(wire['config'])
660 648 output = io.BytesIO()
661 649
662 650 def write(data, **unused_kwargs):
663 651 output.write(data)
664 652
665 653 baseui.write = write
666 654 if branch:
667 655 args = [branch]
668 656 else:
669 657 args = []
670 658 commands.heads(baseui, repo, template='{node} ', *args)
671 659
672 660 return output.getvalue()
673 661
674 662 @reraise_safe_exceptions
675 663 def ancestor(self, wire, revision1, revision2):
676 664 repo = self._factory.repo(wire)
677 665 changelog = repo.changelog
678 666 lookup = repo.lookup
679 667 a = changelog.ancestor(lookup(revision1), lookup(revision2))
680 668 return hex(a)
681 669
682 670 @reraise_safe_exceptions
683 671 def push(self, wire, revisions, dest_path, hooks=True,
684 672 push_branches=False):
685 673 repo = self._factory.repo(wire)
686 674 baseui = self._factory._create_config(wire['config'], hooks=hooks)
687 675 commands.push(baseui, repo, dest=dest_path, rev=revisions,
688 676 new_branch=push_branches)
689 677
690 678 @reraise_safe_exceptions
691 679 def merge(self, wire, revision):
692 680 repo = self._factory.repo(wire)
693 681 baseui = self._factory._create_config(wire['config'])
694 682 repo.ui.setconfig('ui', 'merge', 'internal:dump')
695 683
696 684 # In case of sub repositories are used mercurial prompts the user in
697 685 # case of merge conflicts or different sub repository sources. By
698 686 # setting the interactive flag to `False` mercurial doesn't prompt the
699 687 # used but instead uses a default value.
700 688 repo.ui.setconfig('ui', 'interactive', False)
701 689
702 690 commands.merge(baseui, repo, rev=revision)
703 691
704 692 @reraise_safe_exceptions
705 693 def commit(self, wire, message, username):
706 694 repo = self._factory.repo(wire)
707 695 baseui = self._factory._create_config(wire['config'])
708 696 repo.ui.setconfig('ui', 'username', username)
709 697 commands.commit(baseui, repo, message=message)
710 698
711 699 @reraise_safe_exceptions
712 700 def rebase(self, wire, source=None, dest=None, abort=False):
713 701 repo = self._factory.repo(wire)
714 702 baseui = self._factory._create_config(wire['config'])
715 703 repo.ui.setconfig('ui', 'merge', 'internal:dump')
716 704 rebase.rebase(
717 705 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
718 706
719 707 @reraise_safe_exceptions
720 708 def bookmark(self, wire, bookmark, revision=None):
721 709 repo = self._factory.repo(wire)
722 710 baseui = self._factory._create_config(wire['config'])
723 711 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
@@ -1,395 +1,404 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 4 # Copyright (C) 2014-2017 RodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 import io
21 import sys
22 import json
23 import logging
20 24 import collections
21 25 import importlib
22 import io
23 import json
24 26 import subprocess
25 import sys
27
26 28 from httplib import HTTPConnection
27 29
28 30
29 31 import mercurial.scmutil
30 32 import mercurial.node
31 33 import Pyro4
32 34 import simplejson as json
33 35
34 36 from vcsserver import exceptions
35 37
38 log = logging.getLogger(__name__)
39
36 40
37 41 class HooksHttpClient(object):
38 42 connection = None
39 43
40 44 def __init__(self, hooks_uri):
41 45 self.hooks_uri = hooks_uri
42 46
43 47 def __call__(self, method, extras):
44 48 connection = HTTPConnection(self.hooks_uri)
45 49 body = self._serialize(method, extras)
46 50 connection.request('POST', '/', body)
47 51 response = connection.getresponse()
48 52 return json.loads(response.read())
49 53
50 54 def _serialize(self, hook_name, extras):
51 55 data = {
52 56 'method': hook_name,
53 57 'extras': extras
54 58 }
55 59 return json.dumps(data)
56 60
57 61
58 62 class HooksDummyClient(object):
59 63 def __init__(self, hooks_module):
60 64 self._hooks_module = importlib.import_module(hooks_module)
61 65
62 66 def __call__(self, hook_name, extras):
63 67 with self._hooks_module.Hooks() as hooks:
64 68 return getattr(hooks, hook_name)(extras)
65 69
66 70
67 71 class HooksPyro4Client(object):
68 72 def __init__(self, hooks_uri):
69 73 self.hooks_uri = hooks_uri
70 74
71 75 def __call__(self, hook_name, extras):
72 76 with Pyro4.Proxy(self.hooks_uri) as hooks:
73 77 return getattr(hooks, hook_name)(extras)
74 78
75 79
76 80 class RemoteMessageWriter(object):
77 81 """Writer base class."""
78 82 def write(message):
79 83 raise NotImplementedError()
80 84
81 85
82 86 class HgMessageWriter(RemoteMessageWriter):
83 87 """Writer that knows how to send messages to mercurial clients."""
84 88
85 89 def __init__(self, ui):
86 90 self.ui = ui
87 91
88 92 def write(self, message):
89 93 # TODO: Check why the quiet flag is set by default.
90 94 old = self.ui.quiet
91 95 self.ui.quiet = False
92 96 self.ui.status(message.encode('utf-8'))
93 97 self.ui.quiet = old
94 98
95 99
96 100 class GitMessageWriter(RemoteMessageWriter):
97 101 """Writer that knows how to send messages to git clients."""
98 102
99 103 def __init__(self, stdout=None):
100 104 self.stdout = stdout or sys.stdout
101 105
102 106 def write(self, message):
103 107 self.stdout.write(message.encode('utf-8'))
104 108
105 109
106 110 def _handle_exception(result):
107 111 exception_class = result.get('exception')
112 exception_traceback = result.get('exception_traceback')
113
114 if exception_traceback:
115 log.error('Got traceback from remote call:%s', exception_traceback)
116
108 117 if exception_class == 'HTTPLockedRC':
109 118 raise exceptions.RepositoryLockedException(*result['exception_args'])
110 119 elif exception_class == 'RepositoryError':
111 120 raise exceptions.VcsException(*result['exception_args'])
112 121 elif exception_class:
113 122 raise Exception('Got remote exception "%s" with args "%s"' %
114 123 (exception_class, result['exception_args']))
115 124
116 125
117 126 def _get_hooks_client(extras):
118 127 if 'hooks_uri' in extras:
119 128 protocol = extras.get('hooks_protocol')
120 129 return (
121 130 HooksHttpClient(extras['hooks_uri'])
122 131 if protocol == 'http'
123 132 else HooksPyro4Client(extras['hooks_uri'])
124 133 )
125 134 else:
126 135 return HooksDummyClient(extras['hooks_module'])
127 136
128 137
129 138 def _call_hook(hook_name, extras, writer):
130 139 hooks = _get_hooks_client(extras)
131 140 result = hooks(hook_name, extras)
132 141 writer.write(result['output'])
133 142 _handle_exception(result)
134 143
135 144 return result['status']
136 145
137 146
138 147 def _extras_from_ui(ui):
139 148 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
140 149 return extras
141 150
142 151
143 152 def repo_size(ui, repo, **kwargs):
144 153 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
145 154
146 155
147 156 def pre_pull(ui, repo, **kwargs):
148 157 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
149 158
150 159
151 160 def post_pull(ui, repo, **kwargs):
152 161 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
153 162
154 163
155 164 def pre_push(ui, repo, node=None, **kwargs):
156 165 extras = _extras_from_ui(ui)
157 166
158 167 rev_data = []
159 168 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
160 169 branches = collections.defaultdict(list)
161 170 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
162 171 branches[branch].append(commit_id)
163 172
164 173 for branch, commits in branches.iteritems():
165 174 old_rev = kwargs.get('node_last') or commits[0]
166 175 rev_data.append({
167 176 'old_rev': old_rev,
168 177 'new_rev': commits[-1],
169 178 'ref': '',
170 179 'type': 'branch',
171 180 'name': branch,
172 181 })
173 182
174 183 extras['commit_ids'] = rev_data
175 184 return _call_hook('pre_push', extras, HgMessageWriter(ui))
176 185
177 186
178 187 def _rev_range_hash(repo, node, with_branch=False):
179 188
180 189 commits = []
181 190 for rev in xrange(repo[node], len(repo)):
182 191 ctx = repo[rev]
183 192 commit_id = mercurial.node.hex(ctx.node())
184 193 branch = ctx.branch()
185 194 if with_branch:
186 195 commits.append((commit_id, branch))
187 196 else:
188 197 commits.append(commit_id)
189 198
190 199 return commits
191 200
192 201
193 202 def post_push(ui, repo, node, **kwargs):
194 203 commit_ids = _rev_range_hash(repo, node)
195 204
196 205 extras = _extras_from_ui(ui)
197 206 extras['commit_ids'] = commit_ids
198 207
199 208 return _call_hook('post_push', extras, HgMessageWriter(ui))
200 209
201 210
202 211 # backward compat
203 212 log_pull_action = post_pull
204 213
205 214 # backward compat
206 215 log_push_action = post_push
207 216
208 217
209 218 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
210 219 """
211 220 Old hook name: keep here for backward compatibility.
212 221
213 222 This is only required when the installed git hooks are not upgraded.
214 223 """
215 224 pass
216 225
217 226
218 227 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
219 228 """
220 229 Old hook name: keep here for backward compatibility.
221 230
222 231 This is only required when the installed git hooks are not upgraded.
223 232 """
224 233 pass
225 234
226 235
227 236 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
228 237
229 238
230 239 def git_pre_pull(extras):
231 240 """
232 241 Pre pull hook.
233 242
234 243 :param extras: dictionary containing the keys defined in simplevcs
235 244 :type extras: dict
236 245
237 246 :return: status code of the hook. 0 for success.
238 247 :rtype: int
239 248 """
240 249 if 'pull' not in extras['hooks']:
241 250 return HookResponse(0, '')
242 251
243 252 stdout = io.BytesIO()
244 253 try:
245 254 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
246 255 except Exception as error:
247 256 status = 128
248 257 stdout.write('ERROR: %s\n' % str(error))
249 258
250 259 return HookResponse(status, stdout.getvalue())
251 260
252 261
253 262 def git_post_pull(extras):
254 263 """
255 264 Post pull hook.
256 265
257 266 :param extras: dictionary containing the keys defined in simplevcs
258 267 :type extras: dict
259 268
260 269 :return: status code of the hook. 0 for success.
261 270 :rtype: int
262 271 """
263 272 if 'pull' not in extras['hooks']:
264 273 return HookResponse(0, '')
265 274
266 275 stdout = io.BytesIO()
267 276 try:
268 277 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
269 278 except Exception as error:
270 279 status = 128
271 280 stdout.write('ERROR: %s\n' % error)
272 281
273 282 return HookResponse(status, stdout.getvalue())
274 283
275 284
276 285 def _parse_git_ref_lines(revision_lines):
277 286 rev_data = []
278 287 for revision_line in revision_lines or []:
279 288 old_rev, new_rev, ref = revision_line.strip().split(' ')
280 289 ref_data = ref.split('/', 2)
281 290 if ref_data[1] in ('tags', 'heads'):
282 291 rev_data.append({
283 292 'old_rev': old_rev,
284 293 'new_rev': new_rev,
285 294 'ref': ref,
286 295 'type': ref_data[1],
287 296 'name': ref_data[2],
288 297 })
289 298 return rev_data
290 299
291 300
292 301 def git_pre_receive(unused_repo_path, revision_lines, env):
293 302 """
294 303 Pre push hook.
295 304
296 305 :param extras: dictionary containing the keys defined in simplevcs
297 306 :type extras: dict
298 307
299 308 :return: status code of the hook. 0 for success.
300 309 :rtype: int
301 310 """
302 311 extras = json.loads(env['RC_SCM_DATA'])
303 312 rev_data = _parse_git_ref_lines(revision_lines)
304 313 if 'push' not in extras['hooks']:
305 314 return 0
306 315 extras['commit_ids'] = rev_data
307 316 return _call_hook('pre_push', extras, GitMessageWriter())
308 317
309 318
310 319 def _run_command(arguments):
311 320 """
312 321 Run the specified command and return the stdout.
313 322
314 323 :param arguments: sequence of program arguments (including the program name)
315 324 :type arguments: list[str]
316 325 """
317 326 # TODO(skreft): refactor this method and all the other similar ones.
318 327 # Probably this should be using subprocessio.
319 328 process = subprocess.Popen(
320 329 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
321 330 stdout, _ = process.communicate()
322 331
323 332 if process.returncode != 0:
324 333 raise Exception(
325 334 'Command %s exited with exit code %s' % (arguments,
326 335 process.returncode))
327 336
328 337 return stdout
329 338
330 339
331 340 def git_post_receive(unused_repo_path, revision_lines, env):
332 341 """
333 342 Post push hook.
334 343
335 344 :param extras: dictionary containing the keys defined in simplevcs
336 345 :type extras: dict
337 346
338 347 :return: status code of the hook. 0 for success.
339 348 :rtype: int
340 349 """
341 350 extras = json.loads(env['RC_SCM_DATA'])
342 351 if 'push' not in extras['hooks']:
343 352 return 0
344 353
345 354 rev_data = _parse_git_ref_lines(revision_lines)
346 355
347 356 git_revs = []
348 357
349 358 # N.B.(skreft): it is ok to just call git, as git before calling a
350 359 # subcommand sets the PATH environment variable so that it point to the
351 360 # correct version of the git executable.
352 361 empty_commit_id = '0' * 40
353 362 for push_ref in rev_data:
354 363 type_ = push_ref['type']
355 364 if type_ == 'heads':
356 365 if push_ref['old_rev'] == empty_commit_id:
357 366
358 367 # Fix up head revision if needed
359 368 cmd = ['git', 'show', 'HEAD']
360 369 try:
361 370 _run_command(cmd)
362 371 except Exception:
363 372 cmd = ['git', 'symbolic-ref', 'HEAD',
364 373 'refs/heads/%s' % push_ref['name']]
365 374 print("Setting default branch to %s" % push_ref['name'])
366 375 _run_command(cmd)
367 376
368 377 cmd = ['git', 'for-each-ref', '--format=%(refname)',
369 378 'refs/heads/*']
370 379 heads = _run_command(cmd)
371 380 heads = heads.replace(push_ref['ref'], '')
372 381 heads = ' '.join(head for head in heads.splitlines() if head)
373 382 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
374 383 '--', push_ref['new_rev'], '--not', heads]
375 384 git_revs.extend(_run_command(cmd).splitlines())
376 385 elif push_ref['new_rev'] == empty_commit_id:
377 386 # delete branch case
378 387 git_revs.append('delete_branch=>%s' % push_ref['name'])
379 388 else:
380 389 cmd = ['git', 'log',
381 390 '{old_rev}..{new_rev}'.format(**push_ref),
382 391 '--reverse', '--pretty=format:%H']
383 392 git_revs.extend(_run_command(cmd).splitlines())
384 393 elif type_ == 'tags':
385 394 git_revs.append('tag=>%s' % push_ref['name'])
386 395
387 396 extras['commit_ids'] = git_revs
388 397
389 398 if 'repo_size' in extras['hooks']:
390 399 try:
391 400 _call_hook('repo_size', extras, GitMessageWriter())
392 401 except:
393 402 pass
394 403
395 404 return _call_hook('post_push', extras, GitMessageWriter())
@@ -1,651 +1,640 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2017 RodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 from __future__ import absolute_import
19 19
20 20 from urllib2 import URLError
21 21 import logging
22 22 import posixpath as vcspath
23 23 import StringIO
24 24 import subprocess
25 25 import urllib
26 26
27 27 import svn.client
28 28 import svn.core
29 29 import svn.delta
30 30 import svn.diff
31 31 import svn.fs
32 32 import svn.repos
33 33
34 34 from vcsserver import svn_diff
35 35 from vcsserver import exceptions
36 from vcsserver.base import RepoFactory
36 from vcsserver.base import RepoFactory, raise_from_original
37 37
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 # Set of svn compatible version flags.
43 43 # Compare with subversion/svnadmin/svnadmin.c
44 44 svn_compatible_versions = set([
45 45 'pre-1.4-compatible',
46 46 'pre-1.5-compatible',
47 47 'pre-1.6-compatible',
48 48 'pre-1.8-compatible',
49 49 ])
50 50
51 51
52 52 def reraise_safe_exceptions(func):
53 53 """Decorator for converting svn exceptions to something neutral."""
54 54 def wrapper(*args, **kwargs):
55 55 try:
56 56 return func(*args, **kwargs)
57 57 except Exception as e:
58 58 if not hasattr(e, '_vcs_kind'):
59 59 log.exception("Unhandled exception in hg remote call")
60 60 raise_from_original(exceptions.UnhandledException)
61 61 raise
62 62 return wrapper
63 63
64 64
65 def raise_from_original(new_type):
66 """
67 Raise a new exception type with original args and traceback.
68 """
69 _, original, traceback = sys.exc_info()
70 try:
71 raise new_type(*original.args), None, traceback
72 finally:
73 del traceback
74
75
76 65 class SubversionFactory(RepoFactory):
77 66
78 67 def _create_repo(self, wire, create, compatible_version):
79 68 path = svn.core.svn_path_canonicalize(wire['path'])
80 69 if create:
81 70 fs_config = {}
82 71 if compatible_version:
83 72 if compatible_version not in svn_compatible_versions:
84 73 raise Exception('Unknown SVN compatible version "{}"'
85 74 .format(compatible_version))
86 75 log.debug('Create SVN repo with compatible version "%s"',
87 76 compatible_version)
88 77 fs_config[compatible_version] = '1'
89 78 repo = svn.repos.create(path, "", "", None, fs_config)
90 79 else:
91 80 repo = svn.repos.open(path)
92 81 return repo
93 82
94 83 def repo(self, wire, create=False, compatible_version=None):
95 84 def create_new_repo():
96 85 return self._create_repo(wire, create, compatible_version)
97 86
98 87 return self._repo(wire, create_new_repo)
99 88
100 89
101 90
102 91 NODE_TYPE_MAPPING = {
103 92 svn.core.svn_node_file: 'file',
104 93 svn.core.svn_node_dir: 'dir',
105 94 }
106 95
107 96
108 97 class SvnRemote(object):
109 98
110 99 def __init__(self, factory, hg_factory=None):
111 100 self._factory = factory
112 101 # TODO: Remove once we do not use internal Mercurial objects anymore
113 102 # for subversion
114 103 self._hg_factory = hg_factory
115 104
116 105 @reraise_safe_exceptions
117 106 def discover_svn_version(self):
118 107 try:
119 108 import svn.core
120 109 svn_ver = svn.core.SVN_VERSION
121 110 except ImportError:
122 111 svn_ver = None
123 112 return svn_ver
124 113
125 114 def check_url(self, url, config_items):
126 115 # this can throw exception if not installed, but we detect this
127 116 from hgsubversion import svnrepo
128 117
129 118 baseui = self._hg_factory._create_config(config_items)
130 119 # uuid function get's only valid UUID from proper repo, else
131 120 # throws exception
132 121 try:
133 122 svnrepo.svnremoterepo(baseui, url).svn.uuid
134 123 except:
135 124 log.debug("Invalid svn url: %s", url)
136 125 raise URLError(
137 126 '"%s" is not a valid Subversion source url.' % (url, ))
138 127 return True
139 128
140 129 def is_path_valid_repository(self, wire, path):
141 130 try:
142 131 svn.repos.open(path)
143 132 except svn.core.SubversionException:
144 133 log.debug("Invalid Subversion path %s", path)
145 134 return False
146 135 return True
147 136
148 137 def lookup(self, wire, revision):
149 138 if revision not in [-1, None, 'HEAD']:
150 139 raise NotImplementedError
151 140 repo = self._factory.repo(wire)
152 141 fs_ptr = svn.repos.fs(repo)
153 142 head = svn.fs.youngest_rev(fs_ptr)
154 143 return head
155 144
156 145 def lookup_interval(self, wire, start_ts, end_ts):
157 146 repo = self._factory.repo(wire)
158 147 fsobj = svn.repos.fs(repo)
159 148 start_rev = None
160 149 end_rev = None
161 150 if start_ts:
162 151 start_ts_svn = apr_time_t(start_ts)
163 152 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
164 153 else:
165 154 start_rev = 1
166 155 if end_ts:
167 156 end_ts_svn = apr_time_t(end_ts)
168 157 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
169 158 else:
170 159 end_rev = svn.fs.youngest_rev(fsobj)
171 160 return start_rev, end_rev
172 161
173 162 def revision_properties(self, wire, revision):
174 163 repo = self._factory.repo(wire)
175 164 fs_ptr = svn.repos.fs(repo)
176 165 return svn.fs.revision_proplist(fs_ptr, revision)
177 166
178 167 def revision_changes(self, wire, revision):
179 168
180 169 repo = self._factory.repo(wire)
181 170 fsobj = svn.repos.fs(repo)
182 171 rev_root = svn.fs.revision_root(fsobj, revision)
183 172
184 173 editor = svn.repos.ChangeCollector(fsobj, rev_root)
185 174 editor_ptr, editor_baton = svn.delta.make_editor(editor)
186 175 base_dir = ""
187 176 send_deltas = False
188 177 svn.repos.replay2(
189 178 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
190 179 editor_ptr, editor_baton, None)
191 180
192 181 added = []
193 182 changed = []
194 183 removed = []
195 184
196 185 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
197 186 for path, change in editor.changes.iteritems():
198 187 # TODO: Decide what to do with directory nodes. Subversion can add
199 188 # empty directories.
200 189
201 190 if change.item_kind == svn.core.svn_node_dir:
202 191 continue
203 192 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
204 193 added.append(path)
205 194 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
206 195 svn.repos.CHANGE_ACTION_REPLACE]:
207 196 changed.append(path)
208 197 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
209 198 removed.append(path)
210 199 else:
211 200 raise NotImplementedError(
212 201 "Action %s not supported on path %s" % (
213 202 change.action, path))
214 203
215 204 changes = {
216 205 'added': added,
217 206 'changed': changed,
218 207 'removed': removed,
219 208 }
220 209 return changes
221 210
222 211 def node_history(self, wire, path, revision, limit):
223 212 cross_copies = False
224 213 repo = self._factory.repo(wire)
225 214 fsobj = svn.repos.fs(repo)
226 215 rev_root = svn.fs.revision_root(fsobj, revision)
227 216
228 217 history_revisions = []
229 218 history = svn.fs.node_history(rev_root, path)
230 219 history = svn.fs.history_prev(history, cross_copies)
231 220 while history:
232 221 __, node_revision = svn.fs.history_location(history)
233 222 history_revisions.append(node_revision)
234 223 if limit and len(history_revisions) >= limit:
235 224 break
236 225 history = svn.fs.history_prev(history, cross_copies)
237 226 return history_revisions
238 227
239 228 def node_properties(self, wire, path, revision):
240 229 repo = self._factory.repo(wire)
241 230 fsobj = svn.repos.fs(repo)
242 231 rev_root = svn.fs.revision_root(fsobj, revision)
243 232 return svn.fs.node_proplist(rev_root, path)
244 233
245 234 def file_annotate(self, wire, path, revision):
246 235 abs_path = 'file://' + urllib.pathname2url(
247 236 vcspath.join(wire['path'], path))
248 237 file_uri = svn.core.svn_path_canonicalize(abs_path)
249 238
250 239 start_rev = svn_opt_revision_value_t(0)
251 240 peg_rev = svn_opt_revision_value_t(revision)
252 241 end_rev = peg_rev
253 242
254 243 annotations = []
255 244
256 245 def receiver(line_no, revision, author, date, line, pool):
257 246 annotations.append((line_no, revision, line))
258 247
259 248 # TODO: Cannot use blame5, missing typemap function in the swig code
260 249 try:
261 250 svn.client.blame2(
262 251 file_uri, peg_rev, start_rev, end_rev,
263 252 receiver, svn.client.create_context())
264 253 except svn.core.SubversionException as exc:
265 254 log.exception("Error during blame operation.")
266 255 raise Exception(
267 256 "Blame not supported or file does not exist at path %s. "
268 257 "Error %s." % (path, exc))
269 258
270 259 return annotations
271 260
272 261 def get_node_type(self, wire, path, rev=None):
273 262 repo = self._factory.repo(wire)
274 263 fs_ptr = svn.repos.fs(repo)
275 264 if rev is None:
276 265 rev = svn.fs.youngest_rev(fs_ptr)
277 266 root = svn.fs.revision_root(fs_ptr, rev)
278 267 node = svn.fs.check_path(root, path)
279 268 return NODE_TYPE_MAPPING.get(node, None)
280 269
281 270 def get_nodes(self, wire, path, revision=None):
282 271 repo = self._factory.repo(wire)
283 272 fsobj = svn.repos.fs(repo)
284 273 if revision is None:
285 274 revision = svn.fs.youngest_rev(fsobj)
286 275 root = svn.fs.revision_root(fsobj, revision)
287 276 entries = svn.fs.dir_entries(root, path)
288 277 result = []
289 278 for entry_path, entry_info in entries.iteritems():
290 279 result.append(
291 280 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
292 281 return result
293 282
294 283 def get_file_content(self, wire, path, rev=None):
295 284 repo = self._factory.repo(wire)
296 285 fsobj = svn.repos.fs(repo)
297 286 if rev is None:
298 287 rev = svn.fs.youngest_revision(fsobj)
299 288 root = svn.fs.revision_root(fsobj, rev)
300 289 content = svn.core.Stream(svn.fs.file_contents(root, path))
301 290 return content.read()
302 291
303 292 def get_file_size(self, wire, path, revision=None):
304 293 repo = self._factory.repo(wire)
305 294 fsobj = svn.repos.fs(repo)
306 295 if revision is None:
307 296 revision = svn.fs.youngest_revision(fsobj)
308 297 root = svn.fs.revision_root(fsobj, revision)
309 298 size = svn.fs.file_length(root, path)
310 299 return size
311 300
312 301 def create_repository(self, wire, compatible_version=None):
313 302 log.info('Creating Subversion repository in path "%s"', wire['path'])
314 303 self._factory.repo(wire, create=True,
315 304 compatible_version=compatible_version)
316 305
317 306 def import_remote_repository(self, wire, src_url):
318 307 repo_path = wire['path']
319 308 if not self.is_path_valid_repository(wire, repo_path):
320 309 raise Exception(
321 310 "Path %s is not a valid Subversion repository." % repo_path)
322 311 # TODO: johbo: URL checks ?
323 312 rdump = subprocess.Popen(
324 313 ['svnrdump', 'dump', '--non-interactive', src_url],
325 314 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
326 315 load = subprocess.Popen(
327 316 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
328 317
329 318 # TODO: johbo: This can be a very long operation, might be better
330 319 # to track some kind of status and provide an api to check if the
331 320 # import is done.
332 321 rdump.wait()
333 322 load.wait()
334 323
335 324 if rdump.returncode != 0:
336 325 errors = rdump.stderr.read()
337 326 log.error('svnrdump dump failed: statuscode %s: message: %s',
338 327 rdump.returncode, errors)
339 328 reason = 'UNKNOWN'
340 329 if 'svnrdump: E230001:' in errors:
341 330 reason = 'INVALID_CERTIFICATE'
342 331 raise Exception(
343 332 'Failed to dump the remote repository from %s.' % src_url,
344 333 reason)
345 334 if load.returncode != 0:
346 335 raise Exception(
347 336 'Failed to load the dump of remote repository from %s.' %
348 337 (src_url, ))
349 338
350 339 def commit(self, wire, message, author, timestamp, updated, removed):
351 340 assert isinstance(message, str)
352 341 assert isinstance(author, str)
353 342
354 343 repo = self._factory.repo(wire)
355 344 fsobj = svn.repos.fs(repo)
356 345
357 346 rev = svn.fs.youngest_rev(fsobj)
358 347 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
359 348 txn_root = svn.fs.txn_root(txn)
360 349
361 350 for node in updated:
362 351 TxnNodeProcessor(node, txn_root).update()
363 352 for node in removed:
364 353 TxnNodeProcessor(node, txn_root).remove()
365 354
366 355 commit_id = svn.repos.fs_commit_txn(repo, txn)
367 356
368 357 if timestamp:
369 358 apr_time = apr_time_t(timestamp)
370 359 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
371 360 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
372 361
373 362 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
374 363 return commit_id
375 364
376 365 def diff(self, wire, rev1, rev2, path1=None, path2=None,
377 366 ignore_whitespace=False, context=3):
378 367
379 368 wire.update(cache=False)
380 369 repo = self._factory.repo(wire)
381 370 diff_creator = SvnDiffer(
382 371 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
383 372 try:
384 373 return diff_creator.generate_diff()
385 374 except svn.core.SubversionException as e:
386 375 log.exception(
387 376 "Error during diff operation operation. "
388 377 "Path might not exist %s, %s" % (path1, path2))
389 378 return ""
390 379
391 380
392 381 class SvnDiffer(object):
393 382 """
394 383 Utility to create diffs based on difflib and the Subversion api
395 384 """
396 385
397 386 binary_content = False
398 387
399 388 def __init__(
400 389 self, repo, src_rev, src_path, tgt_rev, tgt_path,
401 390 ignore_whitespace, context):
402 391 self.repo = repo
403 392 self.ignore_whitespace = ignore_whitespace
404 393 self.context = context
405 394
406 395 fsobj = svn.repos.fs(repo)
407 396
408 397 self.tgt_rev = tgt_rev
409 398 self.tgt_path = tgt_path or ''
410 399 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
411 400 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
412 401
413 402 self.src_rev = src_rev
414 403 self.src_path = src_path or self.tgt_path
415 404 self.src_root = svn.fs.revision_root(fsobj, src_rev)
416 405 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
417 406
418 407 self._validate()
419 408
420 409 def _validate(self):
421 410 if (self.tgt_kind != svn.core.svn_node_none and
422 411 self.src_kind != svn.core.svn_node_none and
423 412 self.src_kind != self.tgt_kind):
424 413 # TODO: johbo: proper error handling
425 414 raise Exception(
426 415 "Source and target are not compatible for diff generation. "
427 416 "Source type: %s, target type: %s" %
428 417 (self.src_kind, self.tgt_kind))
429 418
430 419 def generate_diff(self):
431 420 buf = StringIO.StringIO()
432 421 if self.tgt_kind == svn.core.svn_node_dir:
433 422 self._generate_dir_diff(buf)
434 423 else:
435 424 self._generate_file_diff(buf)
436 425 return buf.getvalue()
437 426
438 427 def _generate_dir_diff(self, buf):
439 428 editor = DiffChangeEditor()
440 429 editor_ptr, editor_baton = svn.delta.make_editor(editor)
441 430 svn.repos.dir_delta2(
442 431 self.src_root,
443 432 self.src_path,
444 433 '', # src_entry
445 434 self.tgt_root,
446 435 self.tgt_path,
447 436 editor_ptr, editor_baton,
448 437 authorization_callback_allow_all,
449 438 False, # text_deltas
450 439 svn.core.svn_depth_infinity, # depth
451 440 False, # entry_props
452 441 False, # ignore_ancestry
453 442 )
454 443
455 444 for path, __, change in sorted(editor.changes):
456 445 self._generate_node_diff(
457 446 buf, change, path, self.tgt_path, path, self.src_path)
458 447
459 448 def _generate_file_diff(self, buf):
460 449 change = None
461 450 if self.src_kind == svn.core.svn_node_none:
462 451 change = "add"
463 452 elif self.tgt_kind == svn.core.svn_node_none:
464 453 change = "delete"
465 454 tgt_base, tgt_path = vcspath.split(self.tgt_path)
466 455 src_base, src_path = vcspath.split(self.src_path)
467 456 self._generate_node_diff(
468 457 buf, change, tgt_path, tgt_base, src_path, src_base)
469 458
470 459 def _generate_node_diff(
471 460 self, buf, change, tgt_path, tgt_base, src_path, src_base):
472 461
473 462 if self.src_rev == self.tgt_rev and tgt_base == src_base:
474 463 # makes consistent behaviour with git/hg to return empty diff if
475 464 # we compare same revisions
476 465 return
477 466
478 467 tgt_full_path = vcspath.join(tgt_base, tgt_path)
479 468 src_full_path = vcspath.join(src_base, src_path)
480 469
481 470 self.binary_content = False
482 471 mime_type = self._get_mime_type(tgt_full_path)
483 472
484 473 if mime_type and not mime_type.startswith('text'):
485 474 self.binary_content = True
486 475 buf.write("=" * 67 + '\n')
487 476 buf.write("Cannot display: file marked as a binary type.\n")
488 477 buf.write("svn:mime-type = %s\n" % mime_type)
489 478 buf.write("Index: %s\n" % (tgt_path, ))
490 479 buf.write("=" * 67 + '\n')
491 480 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
492 481 'tgt_path': tgt_path})
493 482
494 483 if change == 'add':
495 484 # TODO: johbo: SVN is missing a zero here compared to git
496 485 buf.write("new file mode 10644\n")
497 486
498 487 #TODO(marcink): intro to binary detection of svn patches
499 488 # if self.binary_content:
500 489 # buf.write('GIT binary patch\n')
501 490
502 491 buf.write("--- /dev/null\t(revision 0)\n")
503 492 src_lines = []
504 493 else:
505 494 if change == 'delete':
506 495 buf.write("deleted file mode 10644\n")
507 496
508 497 #TODO(marcink): intro to binary detection of svn patches
509 498 # if self.binary_content:
510 499 # buf.write('GIT binary patch\n')
511 500
512 501 buf.write("--- a/%s\t(revision %s)\n" % (
513 502 src_path, self.src_rev))
514 503 src_lines = self._svn_readlines(self.src_root, src_full_path)
515 504
516 505 if change == 'delete':
517 506 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
518 507 tgt_lines = []
519 508 else:
520 509 buf.write("+++ b/%s\t(revision %s)\n" % (
521 510 tgt_path, self.tgt_rev))
522 511 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
523 512
524 513 if not self.binary_content:
525 514 udiff = svn_diff.unified_diff(
526 515 src_lines, tgt_lines, context=self.context,
527 516 ignore_blank_lines=self.ignore_whitespace,
528 517 ignore_case=False,
529 518 ignore_space_changes=self.ignore_whitespace)
530 519 buf.writelines(udiff)
531 520
532 521 def _get_mime_type(self, path):
533 522 try:
534 523 mime_type = svn.fs.node_prop(
535 524 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
536 525 except svn.core.SubversionException:
537 526 mime_type = svn.fs.node_prop(
538 527 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
539 528 return mime_type
540 529
541 530 def _svn_readlines(self, fs_root, node_path):
542 531 if self.binary_content:
543 532 return []
544 533 node_kind = svn.fs.check_path(fs_root, node_path)
545 534 if node_kind not in (
546 535 svn.core.svn_node_file, svn.core.svn_node_symlink):
547 536 return []
548 537 content = svn.core.Stream(
549 538 svn.fs.file_contents(fs_root, node_path)).read()
550 539 return content.splitlines(True)
551 540
552 541
553 542 class DiffChangeEditor(svn.delta.Editor):
554 543 """
555 544 Records changes between two given revisions
556 545 """
557 546
558 547 def __init__(self):
559 548 self.changes = []
560 549
561 550 def delete_entry(self, path, revision, parent_baton, pool=None):
562 551 self.changes.append((path, None, 'delete'))
563 552
564 553 def add_file(
565 554 self, path, parent_baton, copyfrom_path, copyfrom_revision,
566 555 file_pool=None):
567 556 self.changes.append((path, 'file', 'add'))
568 557
569 558 def open_file(self, path, parent_baton, base_revision, file_pool=None):
570 559 self.changes.append((path, 'file', 'change'))
571 560
572 561
573 562 def authorization_callback_allow_all(root, path, pool):
574 563 return True
575 564
576 565
577 566 class TxnNodeProcessor(object):
578 567 """
579 568 Utility to process the change of one node within a transaction root.
580 569
581 570 It encapsulates the knowledge of how to add, update or remove
582 571 a node for a given transaction root. The purpose is to support the method
583 572 `SvnRemote.commit`.
584 573 """
585 574
586 575 def __init__(self, node, txn_root):
587 576 assert isinstance(node['path'], str)
588 577
589 578 self.node = node
590 579 self.txn_root = txn_root
591 580
592 581 def update(self):
593 582 self._ensure_parent_dirs()
594 583 self._add_file_if_node_does_not_exist()
595 584 self._update_file_content()
596 585 self._update_file_properties()
597 586
598 587 def remove(self):
599 588 svn.fs.delete(self.txn_root, self.node['path'])
600 589 # TODO: Clean up directory if empty
601 590
602 591 def _ensure_parent_dirs(self):
603 592 curdir = vcspath.dirname(self.node['path'])
604 593 dirs_to_create = []
605 594 while not self._svn_path_exists(curdir):
606 595 dirs_to_create.append(curdir)
607 596 curdir = vcspath.dirname(curdir)
608 597
609 598 for curdir in reversed(dirs_to_create):
610 599 log.debug('Creating missing directory "%s"', curdir)
611 600 svn.fs.make_dir(self.txn_root, curdir)
612 601
613 602 def _svn_path_exists(self, path):
614 603 path_status = svn.fs.check_path(self.txn_root, path)
615 604 return path_status != svn.core.svn_node_none
616 605
617 606 def _add_file_if_node_does_not_exist(self):
618 607 kind = svn.fs.check_path(self.txn_root, self.node['path'])
619 608 if kind == svn.core.svn_node_none:
620 609 svn.fs.make_file(self.txn_root, self.node['path'])
621 610
622 611 def _update_file_content(self):
623 612 assert isinstance(self.node['content'], str)
624 613 handler, baton = svn.fs.apply_textdelta(
625 614 self.txn_root, self.node['path'], None, None)
626 615 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
627 616
628 617 def _update_file_properties(self):
629 618 properties = self.node.get('properties', {})
630 619 for key, value in properties.iteritems():
631 620 svn.fs.change_node_prop(
632 621 self.txn_root, self.node['path'], key, value)
633 622
634 623
635 624 def apr_time_t(timestamp):
636 625 """
637 626 Convert a Python timestamp into APR timestamp type apr_time_t
638 627 """
639 628 return timestamp * 1E6
640 629
641 630
642 631 def svn_opt_revision_value_t(num):
643 632 """
644 633 Put `num` into a `svn_opt_revision_value_t` structure.
645 634 """
646 635 value = svn.core.svn_opt_revision_value_t()
647 636 value.number = num
648 637 revision = svn.core.svn_opt_revision_t()
649 638 revision.kind = svn.core.svn_opt_revision_number
650 639 revision.value = value
651 640 return revision
General Comments 0
You need to be logged in to leave comments. Login now