##// END OF EJS Templates
vcs: use single base for shared functions of Remote objects
marcink -
r749:34388801 default
parent child Browse files
Show More
@@ -0,0 +1,28 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2019 RhodeCode GmbH
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
18
19 class RemoteBase(object):
20 EMPTY_COMMIT = '0' * 40
21
22 def _cache_on(self, wire):
23 context = wire.get('context', '')
24 context_uid = '{}'.format(context)
25 repo_id = wire.get('repo_id', '')
26 cache = wire.get('cache', True)
27 cache_on = context and cache
28 return cache_on, context_uid, repo_id
@@ -1,1094 +1,1085 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode 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 collections
19 19 import logging
20 20 import os
21 21 import posixpath as vcspath
22 22 import re
23 23 import stat
24 24 import traceback
25 25 import urllib
26 26 import urllib2
27 27 from functools import wraps
28 28
29 29 import more_itertools
30 30 import pygit2
31 31 from pygit2 import Repository as LibGit2Repo
32 32 from dulwich import index, objects
33 33 from dulwich.client import HttpGitClient, LocalGitClient
34 34 from dulwich.errors import (
35 35 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 36 MissingCommitError, ObjectMissing, HangupException,
37 37 UnexpectedCommandError)
38 38 from dulwich.repo import Repo as DulwichRepo
39 39 from dulwich.server import update_server_info
40 40
41 41 from vcsserver import exceptions, settings, subprocessio
42 42 from vcsserver.utils import safe_str, safe_int
43 43 from vcsserver.base import RepoFactory, obfuscate_qs
44 44 from vcsserver.hgcompat import (
45 45 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 46 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.vcs_base import RemoteBase
47 48
48 49 DIR_STAT = stat.S_IFDIR
49 50 FILE_MODE = stat.S_IFMT
50 51 GIT_LINK = objects.S_IFGITLINK
51 52 PEELED_REF_MARKER = '^{}'
52 53
53 54
54 55 log = logging.getLogger(__name__)
55 56
56 57
57 58 def str_to_dulwich(value):
58 59 """
59 60 Dulwich 0.10.1a requires `unicode` objects to be passed in.
60 61 """
61 62 return value.decode(settings.WIRE_ENCODING)
62 63
63 64
64 65 def reraise_safe_exceptions(func):
65 66 """Converts Dulwich exceptions to something neutral."""
66 67
67 68 @wraps(func)
68 69 def wrapper(*args, **kwargs):
69 70 try:
70 71 return func(*args, **kwargs)
71 72 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
72 73 exc = exceptions.LookupException(org_exc=e)
73 74 raise exc(safe_str(e))
74 75 except (HangupException, UnexpectedCommandError) as e:
75 76 exc = exceptions.VcsException(org_exc=e)
76 77 raise exc(safe_str(e))
77 78 except Exception as e:
78 79 # NOTE(marcink): becuase of how dulwich handles some exceptions
79 80 # (KeyError on empty repos), we cannot track this and catch all
80 81 # exceptions, it's an exceptions from other handlers
81 82 #if not hasattr(e, '_vcs_kind'):
82 83 #log.exception("Unhandled exception in git remote call")
83 84 #raise_from_original(exceptions.UnhandledException)
84 85 raise
85 86 return wrapper
86 87
87 88
88 89 class Repo(DulwichRepo):
89 90 """
90 91 A wrapper for dulwich Repo class.
91 92
92 93 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
93 94 "Too many open files" error. We need to close all opened file descriptors
94 95 once the repo object is destroyed.
95 96 """
96 97 def __del__(self):
97 98 if hasattr(self, 'object_store'):
98 99 self.close()
99 100
100 101
101 102 class Repository(LibGit2Repo):
102 103
103 104 def __enter__(self):
104 105 return self
105 106
106 107 def __exit__(self, exc_type, exc_val, exc_tb):
107 108 self.free()
108 109
109 110
110 111 class GitFactory(RepoFactory):
111 112 repo_type = 'git'
112 113
113 114 def _create_repo(self, wire, create, use_libgit2=False):
114 115 if use_libgit2:
115 116 return Repository(wire['path'])
116 117 else:
117 118 repo_path = str_to_dulwich(wire['path'])
118 119 return Repo(repo_path)
119 120
120 121 def repo(self, wire, create=False, use_libgit2=False):
121 122 """
122 123 Get a repository instance for the given path.
123 124 """
124 125 return self._create_repo(wire, create, use_libgit2)
125 126
126 127 def repo_libgit2(self, wire):
127 128 return self.repo(wire, use_libgit2=True)
128 129
129 130
130 class GitRemote(object):
131 EMPTY_COMMIT = '0' * 40
131 class GitRemote(RemoteBase):
132 132
133 133 def __init__(self, factory):
134 134 self._factory = factory
135 135 self._bulk_methods = {
136 136 "date": self.date,
137 137 "author": self.author,
138 138 "branch": self.branch,
139 139 "message": self.message,
140 140 "parents": self.parents,
141 141 "_commit": self.revision,
142 142 }
143 143 self.region = self._factory._cache_region
144 144
145 145 def _wire_to_config(self, wire):
146 146 if 'config' in wire:
147 147 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
148 148 return {}
149 149
150 150 def _remote_conf(self, config):
151 151 params = [
152 152 '-c', 'core.askpass=""',
153 153 ]
154 154 ssl_cert_dir = config.get('vcs_ssl_dir')
155 155 if ssl_cert_dir:
156 156 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
157 157 return params
158 158
159 def _cache_on(self, wire):
160 context = wire.get('context', '')
161 context_uid = '{}'.format(context)
162 repo_id = wire.get('repo_id', '')
163 cache = wire.get('cache', True)
164 cache_on = context and cache
165 return cache_on, context_uid, repo_id
166
167 159 @reraise_safe_exceptions
168 160 def discover_git_version(self):
169 161 stdout, _ = self.run_git_command(
170 162 {}, ['--version'], _bare=True, _safe=True)
171 163 prefix = 'git version'
172 164 if stdout.startswith(prefix):
173 165 stdout = stdout[len(prefix):]
174 166 return stdout.strip()
175 167
176 168 @reraise_safe_exceptions
177 169 def is_empty(self, wire):
178 170 repo_init = self._factory.repo_libgit2(wire)
179 171 with repo_init as repo:
180 172
181 173 try:
182 174 has_head = repo.head.name
183 175 if has_head:
184 176 return False
185 177
186 178 # NOTE(marcink): check again using more expensive method
187 179 return repo.is_empty
188 180 except Exception:
189 181 pass
190 182
191 183 return True
192 184
193 185 @reraise_safe_exceptions
194 186 def assert_correct_path(self, wire):
195 187 cache_on, context_uid, repo_id = self._cache_on(wire)
196 188 @self.region.conditional_cache_on_arguments(condition=cache_on)
197 189 def _assert_correct_path(_context_uid, _repo_id):
198 190 try:
199 191 repo_init = self._factory.repo_libgit2(wire)
200 192 with repo_init as repo:
201 193 pass
202 194 except pygit2.GitError:
203 195 path = wire.get('path')
204 196 tb = traceback.format_exc()
205 197 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
206 198 return False
207 199
208 200 return True
209 201 return _assert_correct_path(context_uid, repo_id)
210 202
211 203 @reraise_safe_exceptions
212 204 def bare(self, wire):
213 205 repo_init = self._factory.repo_libgit2(wire)
214 206 with repo_init as repo:
215 207 return repo.is_bare
216 208
217 209 @reraise_safe_exceptions
218 210 def blob_as_pretty_string(self, wire, sha):
219 211 repo_init = self._factory.repo_libgit2(wire)
220 212 with repo_init as repo:
221 213 blob_obj = repo[sha]
222 214 blob = blob_obj.data
223 215 return blob
224 216
225 217 @reraise_safe_exceptions
226 218 def blob_raw_length(self, wire, sha):
227 219 cache_on, context_uid, repo_id = self._cache_on(wire)
228 220 @self.region.conditional_cache_on_arguments(condition=cache_on)
229 221 def _blob_raw_length(_repo_id, _sha):
230 222
231 223 repo_init = self._factory.repo_libgit2(wire)
232 224 with repo_init as repo:
233 225 blob = repo[sha]
234 226 return blob.size
235 227
236 228 return _blob_raw_length(repo_id, sha)
237 229
238 230 def _parse_lfs_pointer(self, raw_content):
239 231
240 232 spec_string = 'version https://git-lfs.github.com/spec'
241 233 if raw_content and raw_content.startswith(spec_string):
242 234 pattern = re.compile(r"""
243 235 (?:\n)?
244 236 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
245 237 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
246 238 ^size[ ](?P<oid_size>[0-9]+)\n
247 239 (?:\n)?
248 240 """, re.VERBOSE | re.MULTILINE)
249 241 match = pattern.match(raw_content)
250 242 if match:
251 243 return match.groupdict()
252 244
253 245 return {}
254 246
255 247 @reraise_safe_exceptions
256 248 def is_large_file(self, wire, commit_id):
257 249
258 250 cache_on, context_uid, repo_id = self._cache_on(wire)
259 251 @self.region.conditional_cache_on_arguments(condition=cache_on)
260 252 def _is_large_file(_repo_id, _sha):
261 253 repo_init = self._factory.repo_libgit2(wire)
262 254 with repo_init as repo:
263 255 blob = repo[commit_id]
264 256 if blob.is_binary:
265 257 return {}
266 258
267 259 return self._parse_lfs_pointer(blob.data)
268 260
269 261 return _is_large_file(repo_id, commit_id)
270 262
271 263 @reraise_safe_exceptions
272 264 def in_largefiles_store(self, wire, oid):
273 265 conf = self._wire_to_config(wire)
274 266 repo_init = self._factory.repo_libgit2(wire)
275 267 with repo_init as repo:
276 268 repo_name = repo.path
277 269
278 270 store_location = conf.get('vcs_git_lfs_store_location')
279 271 if store_location:
280 272
281 273 store = LFSOidStore(
282 274 oid=oid, repo=repo_name, store_location=store_location)
283 275 return store.has_oid()
284 276
285 277 return False
286 278
287 279 @reraise_safe_exceptions
288 280 def store_path(self, wire, oid):
289 281 conf = self._wire_to_config(wire)
290 282 repo_init = self._factory.repo_libgit2(wire)
291 283 with repo_init as repo:
292 284 repo_name = repo.path
293 285
294 286 store_location = conf.get('vcs_git_lfs_store_location')
295 287 if store_location:
296 288 store = LFSOidStore(
297 289 oid=oid, repo=repo_name, store_location=store_location)
298 290 return store.oid_path
299 291 raise ValueError('Unable to fetch oid with path {}'.format(oid))
300 292
301 293 @reraise_safe_exceptions
302 294 def bulk_request(self, wire, rev, pre_load):
303 295 cache_on, context_uid, repo_id = self._cache_on(wire)
304 296 @self.region.conditional_cache_on_arguments(condition=cache_on)
305 297 def _bulk_request(_repo_id, _rev, _pre_load):
306 298 result = {}
307 299 for attr in pre_load:
308 300 try:
309 301 method = self._bulk_methods[attr]
310 302 args = [wire, rev]
311 303 result[attr] = method(*args)
312 304 except KeyError as e:
313 305 raise exceptions.VcsException(e)(
314 306 "Unknown bulk attribute: %s" % attr)
315 307 return result
316 308
317 309 return _bulk_request(repo_id, rev, sorted(pre_load))
318 310
319 311 def _build_opener(self, url):
320 312 handlers = []
321 313 url_obj = url_parser(url)
322 314 _, authinfo = url_obj.authinfo()
323 315
324 316 if authinfo:
325 317 # create a password manager
326 318 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
327 319 passmgr.add_password(*authinfo)
328 320
329 321 handlers.extend((httpbasicauthhandler(passmgr),
330 322 httpdigestauthhandler(passmgr)))
331 323
332 324 return urllib2.build_opener(*handlers)
333 325
334 326 def _type_id_to_name(self, type_id):
335 327 return {
336 328 1: b'commit',
337 329 2: b'tree',
338 330 3: b'blob',
339 331 4: b'tag'
340 332 }[type_id]
341 333
342 334 @reraise_safe_exceptions
343 335 def check_url(self, url, config):
344 336 url_obj = url_parser(url)
345 337 test_uri, _ = url_obj.authinfo()
346 338 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
347 339 url_obj.query = obfuscate_qs(url_obj.query)
348 340 cleaned_uri = str(url_obj)
349 341 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
350 342
351 343 if not test_uri.endswith('info/refs'):
352 344 test_uri = test_uri.rstrip('/') + '/info/refs'
353 345
354 346 o = self._build_opener(url)
355 347 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
356 348
357 349 q = {"service": 'git-upload-pack'}
358 350 qs = '?%s' % urllib.urlencode(q)
359 351 cu = "%s%s" % (test_uri, qs)
360 352 req = urllib2.Request(cu, None, {})
361 353
362 354 try:
363 355 log.debug("Trying to open URL %s", cleaned_uri)
364 356 resp = o.open(req)
365 357 if resp.code != 200:
366 358 raise exceptions.URLError()('Return Code is not 200')
367 359 except Exception as e:
368 360 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
369 361 # means it cannot be cloned
370 362 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
371 363
372 364 # now detect if it's proper git repo
373 365 gitdata = resp.read()
374 366 if 'service=git-upload-pack' in gitdata:
375 367 pass
376 368 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
377 369 # old style git can return some other format !
378 370 pass
379 371 else:
380 372 raise exceptions.URLError()(
381 373 "url [%s] does not look like an git" % (cleaned_uri,))
382 374
383 375 return True
384 376
385 377 @reraise_safe_exceptions
386 378 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
387 379 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
388 380 remote_refs = self.pull(wire, url, apply_refs=False)
389 381 repo = self._factory.repo(wire)
390 382 if isinstance(valid_refs, list):
391 383 valid_refs = tuple(valid_refs)
392 384
393 385 for k in remote_refs:
394 386 # only parse heads/tags and skip so called deferred tags
395 387 if k.startswith(valid_refs) and not k.endswith(deferred):
396 388 repo[k] = remote_refs[k]
397 389
398 390 if update_after_clone:
399 391 # we want to checkout HEAD
400 392 repo["HEAD"] = remote_refs["HEAD"]
401 393 index.build_index_from_tree(repo.path, repo.index_path(),
402 394 repo.object_store, repo["HEAD"].tree)
403 395
404 396 @reraise_safe_exceptions
405 397 def branch(self, wire, commit_id):
406 398 cache_on, context_uid, repo_id = self._cache_on(wire)
407 cache_on = False
408 399 @self.region.conditional_cache_on_arguments(condition=cache_on)
409 400 def _branch(_context_uid, _repo_id, _commit_id):
410 401 regex = re.compile('^refs/heads')
411 402
412 403 def filter_with(ref):
413 404 return regex.match(ref[0]) and ref[1] == _commit_id
414 405
415 406 branches = filter(filter_with, self.get_refs(wire).items())
416 407 return [x[0].split('refs/heads/')[-1] for x in branches]
417 408
418 409 return _branch(context_uid, repo_id, commit_id)
419 410
420 411 @reraise_safe_exceptions
421 412 def commit_branches(self, wire, commit_id):
422 413 cache_on, context_uid, repo_id = self._cache_on(wire)
423 414 @self.region.conditional_cache_on_arguments(condition=cache_on)
424 415 def _commit_branches(_context_uid, _repo_id, _commit_id):
425 416 repo_init = self._factory.repo_libgit2(wire)
426 417 with repo_init as repo:
427 418 branches = [x for x in repo.branches.with_commit(_commit_id)]
428 419 return branches
429 420
430 421 return _commit_branches(context_uid, repo_id, commit_id)
431 422
432 423 @reraise_safe_exceptions
433 424 def add_object(self, wire, content):
434 425 repo_init = self._factory.repo_libgit2(wire)
435 426 with repo_init as repo:
436 427 blob = objects.Blob()
437 428 blob.set_raw_string(content)
438 429 repo.object_store.add_object(blob)
439 430 return blob.id
440 431
441 432 # TODO: this is quite complex, check if that can be simplified
442 433 @reraise_safe_exceptions
443 434 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
444 435 repo = self._factory.repo(wire)
445 436 object_store = repo.object_store
446 437
447 438 # Create tree and populates it with blobs
448 439 commit_tree = commit_tree and repo[commit_tree] or objects.Tree()
449 440
450 441 for node in updated:
451 442 # Compute subdirs if needed
452 443 dirpath, nodename = vcspath.split(node['path'])
453 444 dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
454 445 parent = commit_tree
455 446 ancestors = [('', parent)]
456 447
457 448 # Tries to dig for the deepest existing tree
458 449 while dirnames:
459 450 curdir = dirnames.pop(0)
460 451 try:
461 452 dir_id = parent[curdir][1]
462 453 except KeyError:
463 454 # put curdir back into dirnames and stops
464 455 dirnames.insert(0, curdir)
465 456 break
466 457 else:
467 458 # If found, updates parent
468 459 parent = repo[dir_id]
469 460 ancestors.append((curdir, parent))
470 461 # Now parent is deepest existing tree and we need to create
471 462 # subtrees for dirnames (in reverse order)
472 463 # [this only applies for nodes from added]
473 464 new_trees = []
474 465
475 466 blob = objects.Blob.from_string(node['content'])
476 467
477 468 if dirnames:
478 469 # If there are trees which should be created we need to build
479 470 # them now (in reverse order)
480 471 reversed_dirnames = list(reversed(dirnames))
481 472 curtree = objects.Tree()
482 473 curtree[node['node_path']] = node['mode'], blob.id
483 474 new_trees.append(curtree)
484 475 for dirname in reversed_dirnames[:-1]:
485 476 newtree = objects.Tree()
486 477 newtree[dirname] = (DIR_STAT, curtree.id)
487 478 new_trees.append(newtree)
488 479 curtree = newtree
489 480 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
490 481 else:
491 482 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
492 483
493 484 new_trees.append(parent)
494 485 # Update ancestors
495 486 reversed_ancestors = reversed(
496 487 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
497 488 for parent, tree, path in reversed_ancestors:
498 489 parent[path] = (DIR_STAT, tree.id)
499 490 object_store.add_object(tree)
500 491
501 492 object_store.add_object(blob)
502 493 for tree in new_trees:
503 494 object_store.add_object(tree)
504 495
505 496 for node_path in removed:
506 497 paths = node_path.split('/')
507 498 tree = commit_tree
508 499 trees = [tree]
509 500 # Traverse deep into the forest...
510 501 for path in paths:
511 502 try:
512 503 obj = repo[tree[path][1]]
513 504 if isinstance(obj, objects.Tree):
514 505 trees.append(obj)
515 506 tree = obj
516 507 except KeyError:
517 508 break
518 509 # Cut down the blob and all rotten trees on the way back...
519 510 for path, tree in reversed(zip(paths, trees)):
520 511 del tree[path]
521 512 if tree:
522 513 # This tree still has elements - don't remove it or any
523 514 # of it's parents
524 515 break
525 516
526 517 object_store.add_object(commit_tree)
527 518
528 519 # Create commit
529 520 commit = objects.Commit()
530 521 commit.tree = commit_tree.id
531 522 for k, v in commit_data.iteritems():
532 523 setattr(commit, k, v)
533 524 object_store.add_object(commit)
534 525
535 526 self.create_branch(wire, branch, commit.id)
536 527
537 528 # dulwich set-ref
538 529 ref = 'refs/heads/%s' % branch
539 530 repo.refs[ref] = commit.id
540 531
541 532 return commit.id
542 533
543 534 @reraise_safe_exceptions
544 535 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
545 536 if url != 'default' and '://' not in url:
546 537 client = LocalGitClient(url)
547 538 else:
548 539 url_obj = url_parser(url)
549 540 o = self._build_opener(url)
550 541 url, _ = url_obj.authinfo()
551 542 client = HttpGitClient(base_url=url, opener=o)
552 543 repo = self._factory.repo(wire)
553 544
554 545 determine_wants = repo.object_store.determine_wants_all
555 546 if refs:
556 547 def determine_wants_requested(references):
557 548 return [references[r] for r in references if r in refs]
558 549 determine_wants = determine_wants_requested
559 550
560 551 try:
561 552 remote_refs = client.fetch(
562 553 path=url, target=repo, determine_wants=determine_wants)
563 554 except NotGitRepository as e:
564 555 log.warning(
565 556 'Trying to fetch from "%s" failed, not a Git repository.', url)
566 557 # Exception can contain unicode which we convert
567 558 raise exceptions.AbortException(e)(repr(e))
568 559
569 560 # mikhail: client.fetch() returns all the remote refs, but fetches only
570 561 # refs filtered by `determine_wants` function. We need to filter result
571 562 # as well
572 563 if refs:
573 564 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
574 565
575 566 if apply_refs:
576 567 # TODO: johbo: Needs proper test coverage with a git repository
577 568 # that contains a tag object, so that we would end up with
578 569 # a peeled ref at this point.
579 570 for k in remote_refs:
580 571 if k.endswith(PEELED_REF_MARKER):
581 572 log.debug("Skipping peeled reference %s", k)
582 573 continue
583 574 repo[k] = remote_refs[k]
584 575
585 576 if refs and not update_after:
586 577 # mikhail: explicitly set the head to the last ref.
587 578 repo['HEAD'] = remote_refs[refs[-1]]
588 579
589 580 if update_after:
590 581 # we want to checkout HEAD
591 582 repo["HEAD"] = remote_refs["HEAD"]
592 583 index.build_index_from_tree(repo.path, repo.index_path(),
593 584 repo.object_store, repo["HEAD"].tree)
594 585 return remote_refs
595 586
596 587 @reraise_safe_exceptions
597 588 def sync_fetch(self, wire, url, refs=None):
598 589 repo = self._factory.repo(wire)
599 590 if refs and not isinstance(refs, (list, tuple)):
600 591 refs = [refs]
601 592 config = self._wire_to_config(wire)
602 593 # get all remote refs we'll use to fetch later
603 594 output, __ = self.run_git_command(
604 595 wire, ['ls-remote', url], fail_on_stderr=False,
605 596 _copts=self._remote_conf(config),
606 597 extra_env={'GIT_TERMINAL_PROMPT': '0'})
607 598
608 599 remote_refs = collections.OrderedDict()
609 600 fetch_refs = []
610 601
611 602 for ref_line in output.splitlines():
612 603 sha, ref = ref_line.split('\t')
613 604 sha = sha.strip()
614 605 if ref in remote_refs:
615 606 # duplicate, skip
616 607 continue
617 608 if ref.endswith(PEELED_REF_MARKER):
618 609 log.debug("Skipping peeled reference %s", ref)
619 610 continue
620 611 # don't sync HEAD
621 612 if ref in ['HEAD']:
622 613 continue
623 614
624 615 remote_refs[ref] = sha
625 616
626 617 if refs and sha in refs:
627 618 # we filter fetch using our specified refs
628 619 fetch_refs.append('{}:{}'.format(ref, ref))
629 620 elif not refs:
630 621 fetch_refs.append('{}:{}'.format(ref, ref))
631 622 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
632 623 if fetch_refs:
633 624 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
634 625 fetch_refs_chunks = list(chunk)
635 626 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
636 627 _out, _err = self.run_git_command(
637 628 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
638 629 fail_on_stderr=False,
639 630 _copts=self._remote_conf(config),
640 631 extra_env={'GIT_TERMINAL_PROMPT': '0'})
641 632
642 633 return remote_refs
643 634
644 635 @reraise_safe_exceptions
645 636 def sync_push(self, wire, url, refs=None):
646 637 if not self.check_url(url, wire):
647 638 return
648 639 config = self._wire_to_config(wire)
649 640 self._factory.repo(wire)
650 641 self.run_git_command(
651 642 wire, ['push', url, '--mirror'], fail_on_stderr=False,
652 643 _copts=self._remote_conf(config),
653 644 extra_env={'GIT_TERMINAL_PROMPT': '0'})
654 645
655 646 @reraise_safe_exceptions
656 647 def get_remote_refs(self, wire, url):
657 648 repo = Repo(url)
658 649 return repo.get_refs()
659 650
660 651 @reraise_safe_exceptions
661 652 def get_description(self, wire):
662 653 repo = self._factory.repo(wire)
663 654 return repo.get_description()
664 655
665 656 @reraise_safe_exceptions
666 657 def get_missing_revs(self, wire, rev1, rev2, path2):
667 658 repo = self._factory.repo(wire)
668 659 LocalGitClient(thin_packs=False).fetch(path2, repo)
669 660
670 661 wire_remote = wire.copy()
671 662 wire_remote['path'] = path2
672 663 repo_remote = self._factory.repo(wire_remote)
673 664 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
674 665
675 666 revs = [
676 667 x.commit.id
677 668 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
678 669 return revs
679 670
680 671 @reraise_safe_exceptions
681 672 def get_object(self, wire, sha):
682 673 cache_on, context_uid, repo_id = self._cache_on(wire)
683 674 @self.region.conditional_cache_on_arguments(condition=cache_on)
684 675 def _get_object(_context_uid, _repo_id, _sha):
685 676 repo_init = self._factory.repo_libgit2(wire)
686 677 with repo_init as repo:
687 678
688 679 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
689 680 try:
690 681 commit = repo.revparse_single(sha)
691 682 except (KeyError, ValueError) as e:
692 683 raise exceptions.LookupException(e)(missing_commit_err)
693 684
694 685 is_tag = False
695 686 if isinstance(commit, pygit2.Tag):
696 687 commit = repo.get(commit.target)
697 688 is_tag = True
698 689
699 690 if not is_tag:
700 691 # check for dangling commit
701 692 branches = [x for x in repo.branches.with_commit(commit.hex)]
702 693 if not branches:
703 694 raise exceptions.LookupException(None)(missing_commit_err)
704 695
705 696 commit_id = commit.hex
706 697 type_id = commit.type
707 698
708 699 return {
709 700 'id': commit_id,
710 701 'type': self._type_id_to_name(type_id),
711 702 'commit_id': commit_id,
712 703 'idx': 0
713 704 }
714 705
715 706 return _get_object(context_uid, repo_id, sha)
716 707
717 708 @reraise_safe_exceptions
718 709 def get_refs(self, wire):
719 710 cache_on, context_uid, repo_id = self._cache_on(wire)
720 711 @self.region.conditional_cache_on_arguments(condition=cache_on)
721 712 def _get_refs(_context_uid, _repo_id):
722 713
723 714 repo_init = self._factory.repo_libgit2(wire)
724 715 with repo_init as repo:
725 716 regex = re.compile('^refs/(heads|tags)/')
726 717 return {x.name: x.target.hex for x in
727 718 filter(lambda ref: regex.match(ref.name) ,repo.listall_reference_objects())}
728 719
729 720 return _get_refs(context_uid, repo_id)
730 721
731 722 @reraise_safe_exceptions
732 723 def get_branch_pointers(self, wire):
733 724 cache_on, context_uid, repo_id = self._cache_on(wire)
734 725 @self.region.conditional_cache_on_arguments(condition=cache_on)
735 726 def _get_branch_pointers(_context_uid, _repo_id):
736 727
737 728 repo_init = self._factory.repo_libgit2(wire)
738 729 regex = re.compile('^refs/heads')
739 730 with repo_init as repo:
740 731 branches = filter(lambda ref: regex.match(ref.name), repo.listall_reference_objects())
741 732 return {x.target.hex: x.shorthand for x in branches}
742 733
743 734 return _get_branch_pointers(context_uid, repo_id)
744 735
745 736 @reraise_safe_exceptions
746 737 def head(self, wire, show_exc=True):
747 738 cache_on, context_uid, repo_id = self._cache_on(wire)
748 739 @self.region.conditional_cache_on_arguments(condition=cache_on)
749 740 def _head(_context_uid, _repo_id, _show_exc):
750 741 repo_init = self._factory.repo_libgit2(wire)
751 742 with repo_init as repo:
752 743 try:
753 744 return repo.head.peel().hex
754 745 except Exception:
755 746 if show_exc:
756 747 raise
757 748 return _head(context_uid, repo_id, show_exc)
758 749
759 750 @reraise_safe_exceptions
760 751 def init(self, wire):
761 752 repo_path = str_to_dulwich(wire['path'])
762 753 self.repo = Repo.init(repo_path)
763 754
764 755 @reraise_safe_exceptions
765 756 def init_bare(self, wire):
766 757 repo_path = str_to_dulwich(wire['path'])
767 758 self.repo = Repo.init_bare(repo_path)
768 759
769 760 @reraise_safe_exceptions
770 761 def revision(self, wire, rev):
771 762
772 763 cache_on, context_uid, repo_id = self._cache_on(wire)
773 764 @self.region.conditional_cache_on_arguments(condition=cache_on)
774 765 def _revision(_context_uid, _repo_id, _rev):
775 766 repo_init = self._factory.repo_libgit2(wire)
776 767 with repo_init as repo:
777 768 commit = repo[rev]
778 769 obj_data = {
779 770 'id': commit.id.hex,
780 771 }
781 772 # tree objects itself don't have tree_id attribute
782 773 if hasattr(commit, 'tree_id'):
783 774 obj_data['tree'] = commit.tree_id.hex
784 775
785 776 return obj_data
786 777 return _revision(context_uid, repo_id, rev)
787 778
788 779 @reraise_safe_exceptions
789 780 def date(self, wire, commit_id):
790 781 cache_on, context_uid, repo_id = self._cache_on(wire)
791 782 @self.region.conditional_cache_on_arguments(condition=cache_on)
792 783 def _date(_repo_id, _commit_id):
793 784 repo_init = self._factory.repo_libgit2(wire)
794 785 with repo_init as repo:
795 786 commit = repo[commit_id]
796 787 # TODO(marcink): check dulwich difference of offset vs timezone
797 788 return [commit.commit_time, commit.commit_time_offset]
798 789 return _date(repo_id, commit_id)
799 790
800 791 @reraise_safe_exceptions
801 792 def author(self, wire, commit_id):
802 793 cache_on, context_uid, repo_id = self._cache_on(wire)
803 794 @self.region.conditional_cache_on_arguments(condition=cache_on)
804 795 def _author(_repo_id, _commit_id):
805 796 repo_init = self._factory.repo_libgit2(wire)
806 797 with repo_init as repo:
807 798 commit = repo[commit_id]
808 799 if commit.author.email:
809 800 return u"{} <{}>".format(commit.author.name, commit.author.email)
810 801
811 802 return u"{}".format(commit.author.raw_name)
812 803 return _author(repo_id, commit_id)
813 804
814 805 @reraise_safe_exceptions
815 806 def message(self, wire, commit_id):
816 807 cache_on, context_uid, repo_id = self._cache_on(wire)
817 808 @self.region.conditional_cache_on_arguments(condition=cache_on)
818 809 def _message(_repo_id, _commit_id):
819 810 repo_init = self._factory.repo_libgit2(wire)
820 811 with repo_init as repo:
821 812 commit = repo[commit_id]
822 813 return commit.message
823 814 return _message(repo_id, commit_id)
824 815
825 816 @reraise_safe_exceptions
826 817 def parents(self, wire, commit_id):
827 818 cache_on, context_uid, repo_id = self._cache_on(wire)
828 819 @self.region.conditional_cache_on_arguments(condition=cache_on)
829 820 def _parents(_repo_id, _commit_id):
830 821 repo_init = self._factory.repo_libgit2(wire)
831 822 with repo_init as repo:
832 823 commit = repo[commit_id]
833 824 return [x.hex for x in commit.parent_ids]
834 825 return _parents(repo_id, commit_id)
835 826
836 827 @reraise_safe_exceptions
837 828 def children(self, wire, commit_id):
838 829 cache_on, context_uid, repo_id = self._cache_on(wire)
839 830 @self.region.conditional_cache_on_arguments(condition=cache_on)
840 831 def _children(_repo_id, _commit_id):
841 832 output, __ = self.run_git_command(
842 833 wire, ['rev-list', '--all', '--children'])
843 834
844 835 child_ids = []
845 836 pat = re.compile(r'^%s' % commit_id)
846 837 for l in output.splitlines():
847 838 if pat.match(l):
848 839 found_ids = l.split(' ')[1:]
849 840 child_ids.extend(found_ids)
850 841
851 842 return child_ids
852 843 return _children(repo_id, commit_id)
853 844
854 845 @reraise_safe_exceptions
855 846 def set_refs(self, wire, key, value):
856 847 repo_init = self._factory.repo_libgit2(wire)
857 848 with repo_init as repo:
858 849 repo.references.create(key, value, force=True)
859 850
860 851 @reraise_safe_exceptions
861 852 def create_branch(self, wire, branch_name, commit_id, force=False):
862 853 repo_init = self._factory.repo_libgit2(wire)
863 854 with repo_init as repo:
864 855 commit = repo[commit_id]
865 856
866 857 if force:
867 858 repo.branches.local.create(branch_name, commit, force=force)
868 859 elif not repo.branches.get(branch_name):
869 860 # create only if that branch isn't existing
870 861 repo.branches.local.create(branch_name, commit, force=force)
871 862
872 863 @reraise_safe_exceptions
873 864 def remove_ref(self, wire, key):
874 865 repo_init = self._factory.repo_libgit2(wire)
875 866 with repo_init as repo:
876 867 repo.references.delete(key)
877 868
878 869 @reraise_safe_exceptions
879 870 def tag_remove(self, wire, tag_name):
880 871 repo_init = self._factory.repo_libgit2(wire)
881 872 with repo_init as repo:
882 873 key = 'refs/tags/{}'.format(tag_name)
883 874 repo.references.delete(key)
884 875
885 876 @reraise_safe_exceptions
886 877 def tree_changes(self, wire, source_id, target_id):
887 878 # TODO(marcink): remove this seems it's only used by tests
888 879 repo = self._factory.repo(wire)
889 880 source = repo[source_id].tree if source_id else None
890 881 target = repo[target_id].tree
891 882 result = repo.object_store.tree_changes(source, target)
892 883 return list(result)
893 884
894 885 @reraise_safe_exceptions
895 886 def tree_and_type_for_path(self, wire, commit_id, path):
896 887
897 888 cache_on, context_uid, repo_id = self._cache_on(wire)
898 889 @self.region.conditional_cache_on_arguments(condition=cache_on)
899 890 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
900 891 repo_init = self._factory.repo_libgit2(wire)
901 892
902 893 with repo_init as repo:
903 894 commit = repo[commit_id]
904 895 try:
905 896 tree = commit.tree[path]
906 897 except KeyError:
907 898 return None, None, None
908 899
909 900 return tree.id.hex, tree.type, tree.filemode
910 901 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
911 902
912 903 @reraise_safe_exceptions
913 904 def tree_items(self, wire, tree_id):
914 905 cache_on, context_uid, repo_id = self._cache_on(wire)
915 906 @self.region.conditional_cache_on_arguments(condition=cache_on)
916 907 def _tree_items(_repo_id, _tree_id):
917 908
918 909 repo_init = self._factory.repo_libgit2(wire)
919 910 with repo_init as repo:
920 911 try:
921 912 tree = repo[tree_id]
922 913 except KeyError:
923 914 raise ObjectMissing('No tree with id: {}'.format(tree_id))
924 915
925 916 result = []
926 917 for item in tree:
927 918 item_sha = item.hex
928 919 item_mode = item.filemode
929 920 item_type = item.type
930 921
931 922 if item_type == 'commit':
932 923 # NOTE(marcink): submodules we translate to 'link' for backward compat
933 924 item_type = 'link'
934 925
935 926 result.append((item.name, item_mode, item_sha, item_type))
936 927 return result
937 928 return _tree_items(repo_id, tree_id)
938 929
939 930 @reraise_safe_exceptions
940 931 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
941 932
942 933 flags = [
943 934 '-U%s' % context, '--full-index', '--binary', '-p',
944 935 '-M', '--abbrev=40']
945 936
946 937 if opt_ignorews:
947 938 flags.append('-w')
948 939
949 940 if commit_id_1 == self.EMPTY_COMMIT:
950 941 cmd = ['show'] + flags + [commit_id_2]
951 942 else:
952 943 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
953 944
954 945 if file_filter:
955 946 cmd.extend(['--', file_filter])
956 947
957 948 diff, __ = self.run_git_command(wire, cmd)
958 949 # If we used 'show' command, strip first few lines (until actual diff
959 950 # starts)
960 951 if commit_id_1 == self.EMPTY_COMMIT:
961 952 lines = diff.splitlines()
962 953 x = 0
963 954 for line in lines:
964 955 if line.startswith('diff'):
965 956 break
966 957 x += 1
967 958 # Append new line just like 'diff' command do
968 959 diff = '\n'.join(lines[x:]) + '\n'
969 960 return diff
970 961
971 962 @reraise_safe_exceptions
972 963 def node_history(self, wire, commit_id, path, limit):
973 964 cache_on, context_uid, repo_id = self._cache_on(wire)
974 965 @self.region.conditional_cache_on_arguments(condition=cache_on)
975 966 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
976 967 # optimize for n==1, rev-list is much faster for that use-case
977 968 if limit == 1:
978 969 cmd = ['rev-list', '-1', commit_id, '--', path]
979 970 else:
980 971 cmd = ['log']
981 972 if limit:
982 973 cmd.extend(['-n', str(safe_int(limit, 0))])
983 974 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
984 975
985 976 output, __ = self.run_git_command(wire, cmd)
986 977 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
987 978
988 979 return [x for x in commit_ids]
989 980 return _node_history(context_uid, repo_id, commit_id, path, limit)
990 981
991 982 @reraise_safe_exceptions
992 983 def node_annotate(self, wire, commit_id, path):
993 984
994 985 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
995 986 # -l ==> outputs long shas (and we need all 40 characters)
996 987 # --root ==> doesn't put '^' character for boundaries
997 988 # -r commit_id ==> blames for the given commit
998 989 output, __ = self.run_git_command(wire, cmd)
999 990
1000 991 result = []
1001 992 for i, blame_line in enumerate(output.split('\n')[:-1]):
1002 993 line_no = i + 1
1003 994 commit_id, line = re.split(r' ', blame_line, 1)
1004 995 result.append((line_no, commit_id, line))
1005 996 return result
1006 997
1007 998 @reraise_safe_exceptions
1008 999 def update_server_info(self, wire):
1009 1000 repo = self._factory.repo(wire)
1010 1001 update_server_info(repo)
1011 1002
1012 1003 @reraise_safe_exceptions
1013 1004 def get_all_commit_ids(self, wire):
1014 1005
1015 1006 cache_on, context_uid, repo_id = self._cache_on(wire)
1016 1007 @self.region.conditional_cache_on_arguments(condition=cache_on)
1017 1008 def _get_all_commit_ids(_context_uid, _repo_id):
1018 1009
1019 1010 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1020 1011 try:
1021 1012 output, __ = self.run_git_command(wire, cmd)
1022 1013 return output.splitlines()
1023 1014 except Exception:
1024 1015 # Can be raised for empty repositories
1025 1016 return []
1026 1017 return _get_all_commit_ids(context_uid, repo_id)
1027 1018
1028 1019 @reraise_safe_exceptions
1029 1020 def run_git_command(self, wire, cmd, **opts):
1030 1021 path = wire.get('path', None)
1031 1022
1032 1023 if path and os.path.isdir(path):
1033 1024 opts['cwd'] = path
1034 1025
1035 1026 if '_bare' in opts:
1036 1027 _copts = []
1037 1028 del opts['_bare']
1038 1029 else:
1039 1030 _copts = ['-c', 'core.quotepath=false', ]
1040 1031 safe_call = False
1041 1032 if '_safe' in opts:
1042 1033 # no exc on failure
1043 1034 del opts['_safe']
1044 1035 safe_call = True
1045 1036
1046 1037 if '_copts' in opts:
1047 1038 _copts.extend(opts['_copts'] or [])
1048 1039 del opts['_copts']
1049 1040
1050 1041 gitenv = os.environ.copy()
1051 1042 gitenv.update(opts.pop('extra_env', {}))
1052 1043 # need to clean fix GIT_DIR !
1053 1044 if 'GIT_DIR' in gitenv:
1054 1045 del gitenv['GIT_DIR']
1055 1046 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1056 1047 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1057 1048
1058 1049 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1059 1050 _opts = {'env': gitenv, 'shell': False}
1060 1051
1061 1052 try:
1062 1053 _opts.update(opts)
1063 1054 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
1064 1055
1065 1056 return ''.join(p), ''.join(p.error)
1066 1057 except (EnvironmentError, OSError) as err:
1067 1058 cmd = ' '.join(cmd) # human friendly CMD
1068 1059 tb_err = ("Couldn't run git command (%s).\n"
1069 1060 "Original error was:%s\n"
1070 1061 "Call options:%s\n"
1071 1062 % (cmd, err, _opts))
1072 1063 log.exception(tb_err)
1073 1064 if safe_call:
1074 1065 return '', err
1075 1066 else:
1076 1067 raise exceptions.VcsException()(tb_err)
1077 1068
1078 1069 @reraise_safe_exceptions
1079 1070 def install_hooks(self, wire, force=False):
1080 1071 from vcsserver.hook_utils import install_git_hooks
1081 1072 bare = self.bare(wire)
1082 1073 path = wire['path']
1083 1074 return install_git_hooks(path, bare, force_create=force)
1084 1075
1085 1076 @reraise_safe_exceptions
1086 1077 def get_hooks_info(self, wire):
1087 1078 from vcsserver.hook_utils import (
1088 1079 get_git_pre_hook_version, get_git_post_hook_version)
1089 1080 bare = self.bare(wire)
1090 1081 path = wire['path']
1091 1082 return {
1092 1083 'pre_version': get_git_pre_hook_version(path, bare),
1093 1084 'post_version': get_git_post_hook_version(path, bare),
1094 1085 }
@@ -1,946 +1,939 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode 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 21 import urllib
22 22 import urllib2
23 23 import traceback
24 24
25 25 from hgext import largefiles, rebase
26 26 from hgext.strip import strip as hgext_strip
27 27 from mercurial import commands
28 28 from mercurial import unionrepo
29 29 from mercurial import verify
30 30
31 31 import vcsserver
32 32 from vcsserver import exceptions
33 33 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
34 34 from vcsserver.hgcompat import (
35 35 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
36 36 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
37 37 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
38 38 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
39 39 RepoLookupError, InterventionRequired, RequirementError)
40 from vcsserver.vcs_base import RemoteBase
40 41
41 42 log = logging.getLogger(__name__)
42 43
43 44
44 45 def make_ui_from_config(repo_config):
45 46
46 47 class LoggingUI(ui.ui):
47 48 def status(self, *msg, **opts):
48 49 log.info(' '.join(msg).rstrip('\n'))
49 50 super(LoggingUI, self).status(*msg, **opts)
50 51
51 52 def warn(self, *msg, **opts):
52 53 log.warn(' '.join(msg).rstrip('\n'))
53 54 super(LoggingUI, self).warn(*msg, **opts)
54 55
55 56 def error(self, *msg, **opts):
56 57 log.error(' '.join(msg).rstrip('\n'))
57 58 super(LoggingUI, self).error(*msg, **opts)
58 59
59 60 def note(self, *msg, **opts):
60 61 log.info(' '.join(msg).rstrip('\n'))
61 62 super(LoggingUI, self).note(*msg, **opts)
62 63
63 64 def debug(self, *msg, **opts):
64 65 log.debug(' '.join(msg).rstrip('\n'))
65 66 super(LoggingUI, self).debug(*msg, **opts)
66 67
67 68 baseui = LoggingUI()
68 69
69 70 # clean the baseui object
70 71 baseui._ocfg = hgconfig.config()
71 72 baseui._ucfg = hgconfig.config()
72 73 baseui._tcfg = hgconfig.config()
73 74
74 75 for section, option, value in repo_config:
75 76 baseui.setconfig(section, option, value)
76 77
77 78 # make our hgweb quiet so it doesn't print output
78 79 baseui.setconfig('ui', 'quiet', 'true')
79 80
80 81 baseui.setconfig('ui', 'paginate', 'never')
81 82 # for better Error reporting of Mercurial
82 83 baseui.setconfig('ui', 'message-output', 'stderr')
83 84
84 85 # force mercurial to only use 1 thread, otherwise it may try to set a
85 86 # signal in a non-main thread, thus generating a ValueError.
86 87 baseui.setconfig('worker', 'numcpus', 1)
87 88
88 89 # If there is no config for the largefiles extension, we explicitly disable
89 90 # it here. This overrides settings from repositories hgrc file. Recent
90 91 # mercurial versions enable largefiles in hgrc on clone from largefile
91 92 # repo.
92 93 if not baseui.hasconfig('extensions', 'largefiles'):
93 94 log.debug('Explicitly disable largefiles extension for repo.')
94 95 baseui.setconfig('extensions', 'largefiles', '!')
95 96
96 97 return baseui
97 98
98 99
99 100 def reraise_safe_exceptions(func):
100 101 """Decorator for converting mercurial exceptions to something neutral."""
101 102
102 103 def wrapper(*args, **kwargs):
103 104 try:
104 105 return func(*args, **kwargs)
105 106 except (Abort, InterventionRequired) as e:
106 107 raise_from_original(exceptions.AbortException(e))
107 108 except RepoLookupError as e:
108 109 raise_from_original(exceptions.LookupException(e))
109 110 except RequirementError as e:
110 111 raise_from_original(exceptions.RequirementException(e))
111 112 except RepoError as e:
112 113 raise_from_original(exceptions.VcsException(e))
113 114 except LookupError as e:
114 115 raise_from_original(exceptions.LookupException(e))
115 116 except Exception as e:
116 117 if not hasattr(e, '_vcs_kind'):
117 118 log.exception("Unhandled exception in hg remote call")
118 119 raise_from_original(exceptions.UnhandledException(e))
119 120
120 121 raise
121 122 return wrapper
122 123
123 124
124 125 class MercurialFactory(RepoFactory):
125 126 repo_type = 'hg'
126 127
127 128 def _create_config(self, config, hooks=True):
128 129 if not hooks:
129 130 hooks_to_clean = frozenset((
130 131 'changegroup.repo_size', 'preoutgoing.pre_pull',
131 132 'outgoing.pull_logger', 'prechangegroup.pre_push'))
132 133 new_config = []
133 134 for section, option, value in config:
134 135 if section == 'hooks' and option in hooks_to_clean:
135 136 continue
136 137 new_config.append((section, option, value))
137 138 config = new_config
138 139
139 140 baseui = make_ui_from_config(config)
140 141 return baseui
141 142
142 143 def _create_repo(self, wire, create):
143 144 baseui = self._create_config(wire["config"])
144 145 return instance(baseui, wire["path"], create)
145 146
146 147 def repo(self, wire, create=False):
147 148 """
148 149 Get a repository instance for the given path.
149 150 """
150 151 return self._create_repo(wire, create)
151 152
152 153
153 class HgRemote(object):
154 class HgRemote(RemoteBase):
154 155
155 156 def __init__(self, factory):
156 157 self._factory = factory
157 158 self._bulk_methods = {
158 159 "affected_files": self.ctx_files,
159 160 "author": self.ctx_user,
160 161 "branch": self.ctx_branch,
161 162 "children": self.ctx_children,
162 163 "date": self.ctx_date,
163 164 "message": self.ctx_description,
164 165 "parents": self.ctx_parents,
165 166 "status": self.ctx_status,
166 167 "obsolete": self.ctx_obsolete,
167 168 "phase": self.ctx_phase,
168 169 "hidden": self.ctx_hidden,
169 170 "_file_paths": self.ctx_list,
170 171 }
171 172 self.region = self._factory._cache_region
172 173
173 174 def _get_ctx(self, repo, ref):
174 175 return get_ctx(repo, ref)
175 176
176 def _cache_on(self, wire):
177 context = wire.get('context', '')
178 context_uid = '{}'.format(context)
179 repo_id = wire.get('repo_id', '')
180 cache = wire.get('cache', True)
181 cache_on = context and cache
182 return cache_on, context_uid, repo_id
183
184 177 @reraise_safe_exceptions
185 178 def discover_hg_version(self):
186 179 from mercurial import util
187 180 return util.version()
188 181
189 182 @reraise_safe_exceptions
190 183 def is_empty(self, wire):
191 184 repo = self._factory.repo(wire)
192 185
193 186 try:
194 187 return len(repo) == 0
195 188 except Exception:
196 189 log.exception("failed to read object_store")
197 190 return False
198 191
199 192 @reraise_safe_exceptions
200 193 def archive_repo(self, archive_path, mtime, file_info, kind):
201 194 if kind == "tgz":
202 195 archiver = archival.tarit(archive_path, mtime, "gz")
203 196 elif kind == "tbz2":
204 197 archiver = archival.tarit(archive_path, mtime, "bz2")
205 198 elif kind == 'zip':
206 199 archiver = archival.zipit(archive_path, mtime)
207 200 else:
208 201 raise exceptions.ArchiveException()(
209 202 'Remote does not support: "%s".' % kind)
210 203
211 204 for f_path, f_mode, f_is_link, f_content in file_info:
212 205 archiver.addfile(f_path, f_mode, f_is_link, f_content)
213 206 archiver.done()
214 207
215 208 @reraise_safe_exceptions
216 209 def bookmarks(self, wire):
217 210 cache_on, context_uid, repo_id = self._cache_on(wire)
218 211 @self.region.conditional_cache_on_arguments(condition=cache_on)
219 212 def _bookmarks(_context_uid, _repo_id):
220 213 repo = self._factory.repo(wire)
221 214 return dict(repo._bookmarks)
222 215
223 216 return _bookmarks(context_uid, repo_id)
224 217
225 218 @reraise_safe_exceptions
226 219 def branches(self, wire, normal, closed):
227 220 cache_on, context_uid, repo_id = self._cache_on(wire)
228 221 @self.region.conditional_cache_on_arguments(condition=cache_on)
229 222 def _branches(_context_uid, _repo_id, _normal, _closed):
230 223 repo = self._factory.repo(wire)
231 224 iter_branches = repo.branchmap().iterbranches()
232 225 bt = {}
233 226 for branch_name, _heads, tip, is_closed in iter_branches:
234 227 if normal and not is_closed:
235 228 bt[branch_name] = tip
236 229 if closed and is_closed:
237 230 bt[branch_name] = tip
238 231
239 232 return bt
240 233
241 234 return _branches(context_uid, repo_id, normal, closed)
242 235
243 236 @reraise_safe_exceptions
244 237 def bulk_request(self, wire, commit_id, pre_load):
245 238 cache_on, context_uid, repo_id = self._cache_on(wire)
246 239 @self.region.conditional_cache_on_arguments(condition=cache_on)
247 240 def _bulk_request(_repo_id, _commit_id, _pre_load):
248 241 result = {}
249 242 for attr in pre_load:
250 243 try:
251 244 method = self._bulk_methods[attr]
252 245 result[attr] = method(wire, commit_id)
253 246 except KeyError as e:
254 247 raise exceptions.VcsException(e)(
255 248 'Unknown bulk attribute: "%s"' % attr)
256 249 return result
257 250
258 251 return _bulk_request(repo_id, commit_id, sorted(pre_load))
259 252
260 253 @reraise_safe_exceptions
261 254 def ctx_branch(self, wire, commit_id):
262 255 cache_on, context_uid, repo_id = self._cache_on(wire)
263 256 @self.region.conditional_cache_on_arguments(condition=cache_on)
264 257 def _ctx_branch(_repo_id, _commit_id):
265 258 repo = self._factory.repo(wire)
266 259 ctx = self._get_ctx(repo, commit_id)
267 260 return ctx.branch()
268 261 return _ctx_branch(repo_id, commit_id)
269 262
270 263 @reraise_safe_exceptions
271 264 def ctx_date(self, wire, commit_id):
272 265 cache_on, context_uid, repo_id = self._cache_on(wire)
273 266 @self.region.conditional_cache_on_arguments(condition=cache_on)
274 267 def _ctx_date(_repo_id, _commit_id):
275 268 repo = self._factory.repo(wire)
276 269 ctx = self._get_ctx(repo, commit_id)
277 270 return ctx.date()
278 271 return _ctx_date(repo_id, commit_id)
279 272
280 273 @reraise_safe_exceptions
281 274 def ctx_description(self, wire, revision):
282 275 repo = self._factory.repo(wire)
283 276 ctx = self._get_ctx(repo, revision)
284 277 return ctx.description()
285 278
286 279 @reraise_safe_exceptions
287 280 def ctx_files(self, wire, commit_id):
288 281 cache_on, context_uid, repo_id = self._cache_on(wire)
289 282 @self.region.conditional_cache_on_arguments(condition=cache_on)
290 283 def _ctx_files(_repo_id, _commit_id):
291 284 repo = self._factory.repo(wire)
292 285 ctx = self._get_ctx(repo, commit_id)
293 286 return ctx.files()
294 287
295 288 return _ctx_files(repo_id, commit_id)
296 289
297 290 @reraise_safe_exceptions
298 291 def ctx_list(self, path, revision):
299 292 repo = self._factory.repo(path)
300 293 ctx = self._get_ctx(repo, revision)
301 294 return list(ctx)
302 295
303 296 @reraise_safe_exceptions
304 297 def ctx_parents(self, wire, commit_id):
305 298 cache_on, context_uid, repo_id = self._cache_on(wire)
306 299 @self.region.conditional_cache_on_arguments(condition=cache_on)
307 300 def _ctx_parents(_repo_id, _commit_id):
308 301 repo = self._factory.repo(wire)
309 302 ctx = self._get_ctx(repo, commit_id)
310 303 return [parent.rev() for parent in ctx.parents()
311 304 if not (parent.hidden() or parent.obsolete())]
312 305
313 306 return _ctx_parents(repo_id, commit_id)
314 307
315 308 @reraise_safe_exceptions
316 309 def ctx_children(self, wire, commit_id):
317 310 cache_on, context_uid, repo_id = self._cache_on(wire)
318 311 @self.region.conditional_cache_on_arguments(condition=cache_on)
319 312 def _ctx_children(_repo_id, _commit_id):
320 313 repo = self._factory.repo(wire)
321 314 ctx = self._get_ctx(repo, commit_id)
322 315 return [child.rev() for child in ctx.children()
323 316 if not (child.hidden() or child.obsolete())]
324 317
325 318 return _ctx_children(repo_id, commit_id)
326 319
327 320 @reraise_safe_exceptions
328 321 def ctx_phase(self, wire, commit_id):
329 322 cache_on, context_uid, repo_id = self._cache_on(wire)
330 323 @self.region.conditional_cache_on_arguments(condition=cache_on)
331 324 def _ctx_phase(_context_uid, _repo_id, _commit_id):
332 325 repo = self._factory.repo(wire)
333 326 ctx = self._get_ctx(repo, commit_id)
334 327 # public=0, draft=1, secret=3
335 328 return ctx.phase()
336 329 return _ctx_phase(context_uid, repo_id, commit_id)
337 330
338 331 @reraise_safe_exceptions
339 332 def ctx_obsolete(self, wire, commit_id):
340 333 cache_on, context_uid, repo_id = self._cache_on(wire)
341 334 @self.region.conditional_cache_on_arguments(condition=cache_on)
342 335 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
343 336 repo = self._factory.repo(wire)
344 337 ctx = self._get_ctx(repo, commit_id)
345 338 return ctx.obsolete()
346 339 return _ctx_obsolete(context_uid, repo_id, commit_id)
347 340
348 341 @reraise_safe_exceptions
349 342 def ctx_hidden(self, wire, commit_id):
350 343 cache_on, context_uid, repo_id = self._cache_on(wire)
351 344 @self.region.conditional_cache_on_arguments(condition=cache_on)
352 345 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
353 346 repo = self._factory.repo(wire)
354 347 ctx = self._get_ctx(repo, commit_id)
355 348 return ctx.hidden()
356 349 return _ctx_hidden(context_uid, repo_id, commit_id)
357 350
358 351 @reraise_safe_exceptions
359 352 def ctx_substate(self, wire, revision):
360 353 repo = self._factory.repo(wire)
361 354 ctx = self._get_ctx(repo, revision)
362 355 return ctx.substate
363 356
364 357 @reraise_safe_exceptions
365 358 def ctx_status(self, wire, revision):
366 359 repo = self._factory.repo(wire)
367 360 ctx = self._get_ctx(repo, revision)
368 361 status = repo[ctx.p1().node()].status(other=ctx.node())
369 362 # object of status (odd, custom named tuple in mercurial) is not
370 363 # correctly serializable, we make it a list, as the underling
371 364 # API expects this to be a list
372 365 return list(status)
373 366
374 367 @reraise_safe_exceptions
375 368 def ctx_user(self, wire, revision):
376 369 repo = self._factory.repo(wire)
377 370 ctx = self._get_ctx(repo, revision)
378 371 return ctx.user()
379 372
380 373 @reraise_safe_exceptions
381 374 def check_url(self, url, config):
382 375 _proto = None
383 376 if '+' in url[:url.find('://')]:
384 377 _proto = url[0:url.find('+')]
385 378 url = url[url.find('+') + 1:]
386 379 handlers = []
387 380 url_obj = url_parser(url)
388 381 test_uri, authinfo = url_obj.authinfo()
389 382 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
390 383 url_obj.query = obfuscate_qs(url_obj.query)
391 384
392 385 cleaned_uri = str(url_obj)
393 386 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
394 387
395 388 if authinfo:
396 389 # create a password manager
397 390 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
398 391 passmgr.add_password(*authinfo)
399 392
400 393 handlers.extend((httpbasicauthhandler(passmgr),
401 394 httpdigestauthhandler(passmgr)))
402 395
403 396 o = urllib2.build_opener(*handlers)
404 397 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
405 398 ('Accept', 'application/mercurial-0.1')]
406 399
407 400 q = {"cmd": 'between'}
408 401 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
409 402 qs = '?%s' % urllib.urlencode(q)
410 403 cu = "%s%s" % (test_uri, qs)
411 404 req = urllib2.Request(cu, None, {})
412 405
413 406 try:
414 407 log.debug("Trying to open URL %s", cleaned_uri)
415 408 resp = o.open(req)
416 409 if resp.code != 200:
417 410 raise exceptions.URLError()('Return Code is not 200')
418 411 except Exception as e:
419 412 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
420 413 # means it cannot be cloned
421 414 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
422 415
423 416 # now check if it's a proper hg repo, but don't do it for svn
424 417 try:
425 418 if _proto == 'svn':
426 419 pass
427 420 else:
428 421 # check for pure hg repos
429 422 log.debug(
430 423 "Verifying if URL is a Mercurial repository: %s",
431 424 cleaned_uri)
432 425 ui = make_ui_from_config(config)
433 426 peer_checker = makepeer(ui, url)
434 427 peer_checker.lookup('tip')
435 428 except Exception as e:
436 429 log.warning("URL is not a valid Mercurial repository: %s",
437 430 cleaned_uri)
438 431 raise exceptions.URLError(e)(
439 432 "url [%s] does not look like an hg repo org_exc: %s"
440 433 % (cleaned_uri, e))
441 434
442 435 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
443 436 return True
444 437
445 438 @reraise_safe_exceptions
446 439 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
447 440 repo = self._factory.repo(wire)
448 441
449 442 if file_filter:
450 443 match_filter = match(file_filter[0], '', [file_filter[1]])
451 444 else:
452 445 match_filter = file_filter
453 446 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
454 447
455 448 try:
456 449 return "".join(patch.diff(
457 450 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts))
458 451 except RepoLookupError as e:
459 452 raise exceptions.LookupException(e)()
460 453
461 454 @reraise_safe_exceptions
462 455 def node_history(self, wire, revision, path, limit):
463 456 cache_on, context_uid, repo_id = self._cache_on(wire)
464 457 @self.region.conditional_cache_on_arguments(condition=cache_on)
465 458 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
466 459 repo = self._factory.repo(wire)
467 460
468 461 ctx = self._get_ctx(repo, revision)
469 462 fctx = ctx.filectx(path)
470 463
471 464 def history_iter():
472 465 limit_rev = fctx.rev()
473 466 for obj in reversed(list(fctx.filelog())):
474 467 obj = fctx.filectx(obj)
475 468 ctx = obj.changectx()
476 469 if ctx.hidden() or ctx.obsolete():
477 470 continue
478 471
479 472 if limit_rev >= obj.rev():
480 473 yield obj
481 474
482 475 history = []
483 476 for cnt, obj in enumerate(history_iter()):
484 477 if limit and cnt >= limit:
485 478 break
486 479 history.append(hex(obj.node()))
487 480
488 481 return [x for x in history]
489 482 return _node_history(context_uid, repo_id, revision, path, limit)
490 483
491 484 @reraise_safe_exceptions
492 485 def node_history_untill(self, wire, revision, path, limit):
493 486 cache_on, context_uid, repo_id = self._cache_on(wire)
494 487 @self.region.conditional_cache_on_arguments(condition=cache_on)
495 488 def _node_history_until(_context_uid, _repo_id):
496 489 repo = self._factory.repo(wire)
497 490 ctx = self._get_ctx(repo, revision)
498 491 fctx = ctx.filectx(path)
499 492
500 493 file_log = list(fctx.filelog())
501 494 if limit:
502 495 # Limit to the last n items
503 496 file_log = file_log[-limit:]
504 497
505 498 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
506 499 return _node_history_until(context_uid, repo_id, revision, path, limit)
507 500
508 501 @reraise_safe_exceptions
509 502 def fctx_annotate(self, wire, revision, path):
510 503 repo = self._factory.repo(wire)
511 504 ctx = self._get_ctx(repo, revision)
512 505 fctx = ctx.filectx(path)
513 506
514 507 result = []
515 508 for i, annotate_obj in enumerate(fctx.annotate(), 1):
516 509 ln_no = i
517 510 sha = hex(annotate_obj.fctx.node())
518 511 content = annotate_obj.text
519 512 result.append((ln_no, sha, content))
520 513 return result
521 514
522 515 @reraise_safe_exceptions
523 516 def fctx_node_data(self, wire, revision, path):
524 517 repo = self._factory.repo(wire)
525 518 ctx = self._get_ctx(repo, revision)
526 519 fctx = ctx.filectx(path)
527 520 return fctx.data()
528 521
529 522 @reraise_safe_exceptions
530 523 def fctx_flags(self, wire, commit_id, path):
531 524 cache_on, context_uid, repo_id = self._cache_on(wire)
532 525 @self.region.conditional_cache_on_arguments(condition=cache_on)
533 526 def _fctx_flags(_repo_id, _commit_id, _path):
534 527 repo = self._factory.repo(wire)
535 528 ctx = self._get_ctx(repo, commit_id)
536 529 fctx = ctx.filectx(path)
537 530 return fctx.flags()
538 531
539 532 return _fctx_flags(repo_id, commit_id, path)
540 533
541 534 @reraise_safe_exceptions
542 535 def fctx_size(self, wire, commit_id, path):
543 536 cache_on, context_uid, repo_id = self._cache_on(wire)
544 537 @self.region.conditional_cache_on_arguments(condition=cache_on)
545 538 def _fctx_size(_repo_id, _revision, _path):
546 539 repo = self._factory.repo(wire)
547 540 ctx = self._get_ctx(repo, commit_id)
548 541 fctx = ctx.filectx(path)
549 542 return fctx.size()
550 543 return _fctx_size(repo_id, commit_id, path)
551 544
552 545 @reraise_safe_exceptions
553 546 def get_all_commit_ids(self, wire, name):
554 547 cache_on, context_uid, repo_id = self._cache_on(wire)
555 548 @self.region.conditional_cache_on_arguments(condition=cache_on)
556 549 def _get_all_commit_ids(_context_uid, _repo_id, _name):
557 550 repo = self._factory.repo(wire)
558 551 repo = repo.filtered(name)
559 552 revs = map(lambda x: hex(x[7]), repo.changelog.index)
560 553 return revs
561 554 return _get_all_commit_ids(context_uid, repo_id, name)
562 555
563 556 @reraise_safe_exceptions
564 557 def get_config_value(self, wire, section, name, untrusted=False):
565 558 repo = self._factory.repo(wire)
566 559 return repo.ui.config(section, name, untrusted=untrusted)
567 560
568 561 @reraise_safe_exceptions
569 562 def is_large_file(self, wire, path):
570 563 cache_on, context_uid, repo_id = self._cache_on(wire)
571 564 @self.region.conditional_cache_on_arguments(condition=cache_on)
572 565 def _is_large_file(_context_uid, _repo_id, _path):
573 566 return largefiles.lfutil.isstandin(path)
574 567
575 568 return _is_large_file(context_uid, repo_id, path)
576 569
577 570 @reraise_safe_exceptions
578 571 def in_largefiles_store(self, wire, sha):
579 572 repo = self._factory.repo(wire)
580 573 return largefiles.lfutil.instore(repo, sha)
581 574
582 575 @reraise_safe_exceptions
583 576 def in_user_cache(self, wire, sha):
584 577 repo = self._factory.repo(wire)
585 578 return largefiles.lfutil.inusercache(repo.ui, sha)
586 579
587 580 @reraise_safe_exceptions
588 581 def store_path(self, wire, sha):
589 582 repo = self._factory.repo(wire)
590 583 return largefiles.lfutil.storepath(repo, sha)
591 584
592 585 @reraise_safe_exceptions
593 586 def link(self, wire, sha, path):
594 587 repo = self._factory.repo(wire)
595 588 largefiles.lfutil.link(
596 589 largefiles.lfutil.usercachepath(repo.ui, sha), path)
597 590
598 591 @reraise_safe_exceptions
599 592 def localrepository(self, wire, create=False):
600 593 self._factory.repo(wire, create=create)
601 594
602 595 @reraise_safe_exceptions
603 596 def lookup(self, wire, revision, both):
604 597 cache_on, context_uid, repo_id = self._cache_on(wire)
605 598 @self.region.conditional_cache_on_arguments(condition=cache_on)
606 599 def _lookup(_context_uid, _repo_id, _revision, _both):
607 600
608 601 repo = self._factory.repo(wire)
609 602 rev = _revision
610 603 if isinstance(rev, int):
611 604 # NOTE(marcink):
612 605 # since Mercurial doesn't support negative indexes properly
613 606 # we need to shift accordingly by one to get proper index, e.g
614 607 # repo[-1] => repo[-2]
615 608 # repo[0] => repo[-1]
616 609 if rev <= 0:
617 610 rev = rev + -1
618 611 try:
619 612 ctx = self._get_ctx(repo, rev)
620 613 except (TypeError, RepoLookupError) as e:
621 614 e._org_exc_tb = traceback.format_exc()
622 615 raise exceptions.LookupException(e)(rev)
623 616 except LookupError as e:
624 617 e._org_exc_tb = traceback.format_exc()
625 618 raise exceptions.LookupException(e)(e.name)
626 619
627 620 if not both:
628 621 return ctx.hex()
629 622
630 623 ctx = repo[ctx.hex()]
631 624 return ctx.hex(), ctx.rev()
632 625
633 626 return _lookup(context_uid, repo_id, revision, both)
634 627
635 628 @reraise_safe_exceptions
636 629 def sync_push(self, wire, url):
637 630 if not self.check_url(url, wire['config']):
638 631 return
639 632
640 633 repo = self._factory.repo(wire)
641 634
642 635 # Disable any prompts for this repo
643 636 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
644 637
645 638 bookmarks = dict(repo._bookmarks).keys()
646 639 remote = peer(repo, {}, url)
647 640 # Disable any prompts for this remote
648 641 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
649 642
650 643 return exchange.push(
651 644 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
652 645
653 646 @reraise_safe_exceptions
654 647 def revision(self, wire, rev):
655 648 repo = self._factory.repo(wire)
656 649 ctx = self._get_ctx(repo, rev)
657 650 return ctx.rev()
658 651
659 652 @reraise_safe_exceptions
660 653 def rev_range(self, wire, commit_filter):
661 654 cache_on, context_uid, repo_id = self._cache_on(wire)
662 655 @self.region.conditional_cache_on_arguments(condition=cache_on)
663 656 def _rev_range(_context_uid, _repo_id, _filter):
664 657 repo = self._factory.repo(wire)
665 658 revisions = [rev for rev in revrange(repo, commit_filter)]
666 659 return revisions
667 660
668 661 return _rev_range(context_uid, repo_id, sorted(commit_filter))
669 662
670 663 @reraise_safe_exceptions
671 664 def rev_range_hash(self, wire, node):
672 665 repo = self._factory.repo(wire)
673 666
674 667 def get_revs(repo, rev_opt):
675 668 if rev_opt:
676 669 revs = revrange(repo, rev_opt)
677 670 if len(revs) == 0:
678 671 return (nullrev, nullrev)
679 672 return max(revs), min(revs)
680 673 else:
681 674 return len(repo) - 1, 0
682 675
683 676 stop, start = get_revs(repo, [node + ':'])
684 677 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
685 678 return revs
686 679
687 680 @reraise_safe_exceptions
688 681 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
689 682 other_path = kwargs.pop('other_path', None)
690 683
691 684 # case when we want to compare two independent repositories
692 685 if other_path and other_path != wire["path"]:
693 686 baseui = self._factory._create_config(wire["config"])
694 687 repo = unionrepo.makeunionrepository(baseui, other_path, wire["path"])
695 688 else:
696 689 repo = self._factory.repo(wire)
697 690 return list(repo.revs(rev_spec, *args))
698 691
699 692 @reraise_safe_exceptions
700 693 def verify(self, wire,):
701 694 repo = self._factory.repo(wire)
702 695 baseui = self._factory._create_config(wire['config'])
703 696 baseui.setconfig('ui', 'quiet', 'false')
704 697 output = io.BytesIO()
705 698
706 699 def write(data, **unused_kwargs):
707 700 output.write(data)
708 701 baseui.write = write
709 702
710 703 repo.ui = baseui
711 704 verify.verify(repo)
712 705 return output.getvalue()
713 706
714 707 @reraise_safe_exceptions
715 708 def tags(self, wire):
716 709 cache_on, context_uid, repo_id = self._cache_on(wire)
717 710 @self.region.conditional_cache_on_arguments(condition=cache_on)
718 711 def _tags(_context_uid, _repo_id):
719 712 repo = self._factory.repo(wire)
720 713 return repo.tags()
721 714
722 715 return _tags(context_uid, repo_id)
723 716
724 717 @reraise_safe_exceptions
725 718 def update(self, wire, node=None, clean=False):
726 719 repo = self._factory.repo(wire)
727 720 baseui = self._factory._create_config(wire['config'])
728 721 commands.update(baseui, repo, node=node, clean=clean)
729 722
730 723 @reraise_safe_exceptions
731 724 def identify(self, wire):
732 725 repo = self._factory.repo(wire)
733 726 baseui = self._factory._create_config(wire['config'])
734 727 output = io.BytesIO()
735 728 baseui.write = output.write
736 729 # This is required to get a full node id
737 730 baseui.debugflag = True
738 731 commands.identify(baseui, repo, id=True)
739 732
740 733 return output.getvalue()
741 734
742 735 @reraise_safe_exceptions
743 736 def heads(self, wire, branch=None):
744 737 repo = self._factory.repo(wire)
745 738 baseui = self._factory._create_config(wire['config'])
746 739 output = io.BytesIO()
747 740
748 741 def write(data, **unused_kwargs):
749 742 output.write(data)
750 743
751 744 baseui.write = write
752 745 if branch:
753 746 args = [branch]
754 747 else:
755 748 args = []
756 749 commands.heads(baseui, repo, template='{node} ', *args)
757 750
758 751 return output.getvalue()
759 752
760 753 @reraise_safe_exceptions
761 754 def ancestor(self, wire, revision1, revision2):
762 755 repo = self._factory.repo(wire)
763 756 changelog = repo.changelog
764 757 lookup = repo.lookup
765 758 a = changelog.ancestor(lookup(revision1), lookup(revision2))
766 759 return hex(a)
767 760
768 761 @reraise_safe_exceptions
769 762 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
770 763 baseui = self._factory._create_config(wire["config"], hooks=hooks)
771 764 clone(baseui, source, dest, noupdate=not update_after_clone)
772 765
773 766 @reraise_safe_exceptions
774 767 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
775 768
776 769 repo = self._factory.repo(wire)
777 770 baseui = self._factory._create_config(wire['config'])
778 771 publishing = baseui.configbool('phases', 'publish')
779 772 if publishing:
780 773 new_commit = 'public'
781 774 else:
782 775 new_commit = 'draft'
783 776
784 777 def _filectxfn(_repo, ctx, path):
785 778 """
786 779 Marks given path as added/changed/removed in a given _repo. This is
787 780 for internal mercurial commit function.
788 781 """
789 782
790 783 # check if this path is removed
791 784 if path in removed:
792 785 # returning None is a way to mark node for removal
793 786 return None
794 787
795 788 # check if this path is added
796 789 for node in updated:
797 790 if node['path'] == path:
798 791 return memfilectx(
799 792 _repo,
800 793 changectx=ctx,
801 794 path=node['path'],
802 795 data=node['content'],
803 796 islink=False,
804 797 isexec=bool(node['mode'] & stat.S_IXUSR),
805 798 copysource=False)
806 799
807 800 raise exceptions.AbortException()(
808 801 "Given path haven't been marked as added, "
809 802 "changed or removed (%s)" % path)
810 803
811 804 with repo.ui.configoverride({('phases', 'new-commit'): new_commit}):
812 805
813 806 commit_ctx = memctx(
814 807 repo=repo,
815 808 parents=parents,
816 809 text=message,
817 810 files=files,
818 811 filectxfn=_filectxfn,
819 812 user=user,
820 813 date=(commit_time, commit_timezone),
821 814 extra=extra)
822 815
823 816 n = repo.commitctx(commit_ctx)
824 817 new_id = hex(n)
825 818
826 819 return new_id
827 820
828 821 @reraise_safe_exceptions
829 822 def pull(self, wire, url, commit_ids=None):
830 823 repo = self._factory.repo(wire)
831 824 # Disable any prompts for this repo
832 825 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
833 826
834 827 remote = peer(repo, {}, url)
835 828 # Disable any prompts for this remote
836 829 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
837 830
838 831 if commit_ids:
839 832 commit_ids = [bin(commit_id) for commit_id in commit_ids]
840 833
841 834 return exchange.pull(
842 835 repo, remote, heads=commit_ids, force=None).cgresult
843 836
844 837 @reraise_safe_exceptions
845 838 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None, hooks=True):
846 839 repo = self._factory.repo(wire)
847 840 baseui = self._factory._create_config(wire['config'], hooks=hooks)
848 841
849 842 # Mercurial internally has a lot of logic that checks ONLY if
850 843 # option is defined, we just pass those if they are defined then
851 844 opts = {}
852 845 if bookmark:
853 846 opts['bookmark'] = bookmark
854 847 if branch:
855 848 opts['branch'] = branch
856 849 if revision:
857 850 opts['rev'] = revision
858 851
859 852 commands.pull(baseui, repo, source, **opts)
860 853
861 854 @reraise_safe_exceptions
862 855 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
863 856 repo = self._factory.repo(wire)
864 857 baseui = self._factory._create_config(wire['config'], hooks=hooks)
865 858 commands.push(baseui, repo, dest=dest_path, rev=revisions,
866 859 new_branch=push_branches)
867 860
868 861 @reraise_safe_exceptions
869 862 def strip(self, wire, revision, update, backup):
870 863 repo = self._factory.repo(wire)
871 864 ctx = self._get_ctx(repo, revision)
872 865 hgext_strip(
873 866 repo.baseui, repo, ctx.node(), update=update, backup=backup)
874 867
875 868 @reraise_safe_exceptions
876 869 def merge(self, wire, revision):
877 870 repo = self._factory.repo(wire)
878 871 baseui = self._factory._create_config(wire['config'])
879 872 repo.ui.setconfig('ui', 'merge', 'internal:dump')
880 873
881 874 # In case of sub repositories are used mercurial prompts the user in
882 875 # case of merge conflicts or different sub repository sources. By
883 876 # setting the interactive flag to `False` mercurial doesn't prompt the
884 877 # used but instead uses a default value.
885 878 repo.ui.setconfig('ui', 'interactive', False)
886 879 commands.merge(baseui, repo, rev=revision)
887 880
888 881 @reraise_safe_exceptions
889 882 def merge_state(self, wire):
890 883 repo = self._factory.repo(wire)
891 884 repo.ui.setconfig('ui', 'merge', 'internal:dump')
892 885
893 886 # In case of sub repositories are used mercurial prompts the user in
894 887 # case of merge conflicts or different sub repository sources. By
895 888 # setting the interactive flag to `False` mercurial doesn't prompt the
896 889 # used but instead uses a default value.
897 890 repo.ui.setconfig('ui', 'interactive', False)
898 891 ms = hg_merge.mergestate(repo)
899 892 return [x for x in ms.unresolved()]
900 893
901 894 @reraise_safe_exceptions
902 895 def commit(self, wire, message, username, close_branch=False):
903 896 repo = self._factory.repo(wire)
904 897 baseui = self._factory._create_config(wire['config'])
905 898 repo.ui.setconfig('ui', 'username', username)
906 899 commands.commit(baseui, repo, message=message, close_branch=close_branch)
907 900
908 901 @reraise_safe_exceptions
909 902 def rebase(self, wire, source=None, dest=None, abort=False):
910 903 repo = self._factory.repo(wire)
911 904 baseui = self._factory._create_config(wire['config'])
912 905 repo.ui.setconfig('ui', 'merge', 'internal:dump')
913 906 rebase.rebase(
914 907 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
915 908
916 909 @reraise_safe_exceptions
917 910 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
918 911 repo = self._factory.repo(wire)
919 912 ctx = self._get_ctx(repo, revision)
920 913 node = ctx.node()
921 914
922 915 date = (tag_time, tag_timezone)
923 916 try:
924 917 hg_tag.tag(repo, name, node, message, local, user, date)
925 918 except Abort as e:
926 919 log.exception("Tag operation aborted")
927 920 # Exception can contain unicode which we convert
928 921 raise exceptions.AbortException(e)(repr(e))
929 922
930 923 @reraise_safe_exceptions
931 924 def bookmark(self, wire, bookmark, revision=None):
932 925 repo = self._factory.repo(wire)
933 926 baseui = self._factory._create_config(wire['config'])
934 927 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
935 928
936 929 @reraise_safe_exceptions
937 930 def install_hooks(self, wire, force=False):
938 931 # we don't need any special hooks for Mercurial
939 932 pass
940 933
941 934 @reraise_safe_exceptions
942 935 def get_hooks_info(self, wire):
943 936 return {
944 937 'pre_version': vcsserver.__version__,
945 938 'post_version': vcsserver.__version__,
946 939 }
@@ -1,796 +1,789 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 RhodeCode 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 import os
21 21 import subprocess
22 22 from urllib2 import URLError
23 23 import urlparse
24 24 import logging
25 25 import posixpath as vcspath
26 26 import StringIO
27 27 import urllib
28 28 import traceback
29 29
30 30 import svn.client
31 31 import svn.core
32 32 import svn.delta
33 33 import svn.diff
34 34 import svn.fs
35 35 import svn.repos
36 36
37 37 from vcsserver import svn_diff, exceptions, subprocessio, settings
38 38 from vcsserver.base import RepoFactory, raise_from_original
39 from vcsserver.vcs_base import RemoteBase
39 40
40 41 log = logging.getLogger(__name__)
41 42
42 43
43 44 # Set of svn compatible version flags.
44 45 # Compare with subversion/svnadmin/svnadmin.c
45 46 svn_compatible_versions = {
46 47 'pre-1.4-compatible',
47 48 'pre-1.5-compatible',
48 49 'pre-1.6-compatible',
49 50 'pre-1.8-compatible',
50 51 'pre-1.9-compatible'
51 52 }
52 53
53 54 svn_compatible_versions_map = {
54 55 'pre-1.4-compatible': '1.3',
55 56 'pre-1.5-compatible': '1.4',
56 57 'pre-1.6-compatible': '1.5',
57 58 'pre-1.8-compatible': '1.7',
58 59 'pre-1.9-compatible': '1.8',
59 60 }
60 61
61 62
62 63 def reraise_safe_exceptions(func):
63 64 """Decorator for converting svn exceptions to something neutral."""
64 65 def wrapper(*args, **kwargs):
65 66 try:
66 67 return func(*args, **kwargs)
67 68 except Exception as e:
68 69 if not hasattr(e, '_vcs_kind'):
69 70 log.exception("Unhandled exception in svn remote call")
70 71 raise_from_original(exceptions.UnhandledException(e))
71 72 raise
72 73 return wrapper
73 74
74 75
75 76 class SubversionFactory(RepoFactory):
76 77 repo_type = 'svn'
77 78
78 79 def _create_repo(self, wire, create, compatible_version):
79 80 path = svn.core.svn_path_canonicalize(wire['path'])
80 81 if create:
81 82 fs_config = {'compatible-version': '1.9'}
82 83 if compatible_version:
83 84 if compatible_version not in svn_compatible_versions:
84 85 raise Exception('Unknown SVN compatible version "{}"'
85 86 .format(compatible_version))
86 87 fs_config['compatible-version'] = \
87 88 svn_compatible_versions_map[compatible_version]
88 89
89 90 log.debug('Create SVN repo with config "%s"', fs_config)
90 91 repo = svn.repos.create(path, "", "", None, fs_config)
91 92 else:
92 93 repo = svn.repos.open(path)
93 94
94 95 log.debug('Got SVN object: %s', repo)
95 96 return repo
96 97
97 98 def repo(self, wire, create=False, compatible_version=None):
98 99 """
99 100 Get a repository instance for the given path.
100 101 """
101 102 return self._create_repo(wire, create, compatible_version)
102 103
103 104
104 105 NODE_TYPE_MAPPING = {
105 106 svn.core.svn_node_file: 'file',
106 107 svn.core.svn_node_dir: 'dir',
107 108 }
108 109
109 110
110 class SvnRemote(object):
111 class SvnRemote(RemoteBase):
111 112
112 113 def __init__(self, factory, hg_factory=None):
113 114 self._factory = factory
114 115 # TODO: Remove once we do not use internal Mercurial objects anymore
115 116 # for subversion
116 117 self._hg_factory = hg_factory
117 118 self.region = self._factory._cache_region
118 119
119 def _cache_on(self, wire):
120 context = wire.get('context', '')
121 context_uid = '{}'.format(context)
122 repo_id = wire.get('repo_id', '')
123 cache = wire.get('cache', True)
124 cache_on = context and cache
125 return cache_on, context_uid, repo_id
126
127 120 @reraise_safe_exceptions
128 121 def discover_svn_version(self):
129 122 try:
130 123 import svn.core
131 124 svn_ver = svn.core.SVN_VERSION
132 125 except ImportError:
133 126 svn_ver = None
134 127 return svn_ver
135 128
136 129 @reraise_safe_exceptions
137 130 def is_empty(self, wire):
138 131
139 132 try:
140 133 return self.lookup(wire, -1) == 0
141 134 except Exception:
142 135 log.exception("failed to read object_store")
143 136 return False
144 137
145 138 def check_url(self, url, config_items):
146 139 # this can throw exception if not installed, but we detect this
147 140 from hgsubversion import svnrepo
148 141
149 142 baseui = self._hg_factory._create_config(config_items)
150 143 # uuid function get's only valid UUID from proper repo, else
151 144 # throws exception
152 145 try:
153 146 svnrepo.svnremoterepo(baseui, url).svn.uuid
154 147 except Exception:
155 148 tb = traceback.format_exc()
156 149 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
157 150 raise URLError(
158 151 '"%s" is not a valid Subversion source url.' % (url, ))
159 152 return True
160 153
161 154 def is_path_valid_repository(self, wire, path):
162 155
163 156 # NOTE(marcink): short circuit the check for SVN repo
164 157 # the repos.open might be expensive to check, but we have one cheap
165 158 # pre condition that we can use, to check for 'format' file
166 159
167 160 if not os.path.isfile(os.path.join(path, 'format')):
168 161 return False
169 162
170 163 try:
171 164 svn.repos.open(path)
172 165 except svn.core.SubversionException:
173 166 tb = traceback.format_exc()
174 167 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
175 168 return False
176 169 return True
177 170
178 171 @reraise_safe_exceptions
179 172 def verify(self, wire,):
180 173 repo_path = wire['path']
181 174 if not self.is_path_valid_repository(wire, repo_path):
182 175 raise Exception(
183 176 "Path %s is not a valid Subversion repository." % repo_path)
184 177
185 178 cmd = ['svnadmin', 'info', repo_path]
186 179 stdout, stderr = subprocessio.run_command(cmd)
187 180 return stdout
188 181
189 182 def lookup(self, wire, revision):
190 183 if revision not in [-1, None, 'HEAD']:
191 184 raise NotImplementedError
192 185 repo = self._factory.repo(wire)
193 186 fs_ptr = svn.repos.fs(repo)
194 187 head = svn.fs.youngest_rev(fs_ptr)
195 188 return head
196 189
197 190 def lookup_interval(self, wire, start_ts, end_ts):
198 191 repo = self._factory.repo(wire)
199 192 fsobj = svn.repos.fs(repo)
200 193 start_rev = None
201 194 end_rev = None
202 195 if start_ts:
203 196 start_ts_svn = apr_time_t(start_ts)
204 197 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
205 198 else:
206 199 start_rev = 1
207 200 if end_ts:
208 201 end_ts_svn = apr_time_t(end_ts)
209 202 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
210 203 else:
211 204 end_rev = svn.fs.youngest_rev(fsobj)
212 205 return start_rev, end_rev
213 206
214 207 def revision_properties(self, wire, revision):
215 208
216 209 cache_on, context_uid, repo_id = self._cache_on(wire)
217 210 @self.region.conditional_cache_on_arguments(condition=cache_on)
218 211 def _revision_properties(_repo_id, _revision):
219 212 repo = self._factory.repo(wire)
220 213 fs_ptr = svn.repos.fs(repo)
221 214 return svn.fs.revision_proplist(fs_ptr, revision)
222 215 return _revision_properties(repo_id, revision)
223 216
224 217 def revision_changes(self, wire, revision):
225 218
226 219 repo = self._factory.repo(wire)
227 220 fsobj = svn.repos.fs(repo)
228 221 rev_root = svn.fs.revision_root(fsobj, revision)
229 222
230 223 editor = svn.repos.ChangeCollector(fsobj, rev_root)
231 224 editor_ptr, editor_baton = svn.delta.make_editor(editor)
232 225 base_dir = ""
233 226 send_deltas = False
234 227 svn.repos.replay2(
235 228 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
236 229 editor_ptr, editor_baton, None)
237 230
238 231 added = []
239 232 changed = []
240 233 removed = []
241 234
242 235 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
243 236 for path, change in editor.changes.iteritems():
244 237 # TODO: Decide what to do with directory nodes. Subversion can add
245 238 # empty directories.
246 239
247 240 if change.item_kind == svn.core.svn_node_dir:
248 241 continue
249 242 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
250 243 added.append(path)
251 244 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
252 245 svn.repos.CHANGE_ACTION_REPLACE]:
253 246 changed.append(path)
254 247 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
255 248 removed.append(path)
256 249 else:
257 250 raise NotImplementedError(
258 251 "Action %s not supported on path %s" % (
259 252 change.action, path))
260 253
261 254 changes = {
262 255 'added': added,
263 256 'changed': changed,
264 257 'removed': removed,
265 258 }
266 259 return changes
267 260
268 261 @reraise_safe_exceptions
269 262 def node_history(self, wire, path, revision, limit):
270 263 cache_on, context_uid, repo_id = self._cache_on(wire)
271 264 @self.region.conditional_cache_on_arguments(condition=cache_on)
272 265 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
273 266 cross_copies = False
274 267 repo = self._factory.repo(wire)
275 268 fsobj = svn.repos.fs(repo)
276 269 rev_root = svn.fs.revision_root(fsobj, revision)
277 270
278 271 history_revisions = []
279 272 history = svn.fs.node_history(rev_root, path)
280 273 history = svn.fs.history_prev(history, cross_copies)
281 274 while history:
282 275 __, node_revision = svn.fs.history_location(history)
283 276 history_revisions.append(node_revision)
284 277 if limit and len(history_revisions) >= limit:
285 278 break
286 279 history = svn.fs.history_prev(history, cross_copies)
287 280 return history_revisions
288 281 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
289 282
290 283 def node_properties(self, wire, path, revision):
291 284 cache_on, context_uid, repo_id = self._cache_on(wire)
292 285 @self.region.conditional_cache_on_arguments(condition=cache_on)
293 286 def _node_properties(_repo_id, _path, _revision):
294 287 repo = self._factory.repo(wire)
295 288 fsobj = svn.repos.fs(repo)
296 289 rev_root = svn.fs.revision_root(fsobj, revision)
297 290 return svn.fs.node_proplist(rev_root, path)
298 291 return _node_properties(repo_id, path, revision)
299 292
300 293 def file_annotate(self, wire, path, revision):
301 294 abs_path = 'file://' + urllib.pathname2url(
302 295 vcspath.join(wire['path'], path))
303 296 file_uri = svn.core.svn_path_canonicalize(abs_path)
304 297
305 298 start_rev = svn_opt_revision_value_t(0)
306 299 peg_rev = svn_opt_revision_value_t(revision)
307 300 end_rev = peg_rev
308 301
309 302 annotations = []
310 303
311 304 def receiver(line_no, revision, author, date, line, pool):
312 305 annotations.append((line_no, revision, line))
313 306
314 307 # TODO: Cannot use blame5, missing typemap function in the swig code
315 308 try:
316 309 svn.client.blame2(
317 310 file_uri, peg_rev, start_rev, end_rev,
318 311 receiver, svn.client.create_context())
319 312 except svn.core.SubversionException as exc:
320 313 log.exception("Error during blame operation.")
321 314 raise Exception(
322 315 "Blame not supported or file does not exist at path %s. "
323 316 "Error %s." % (path, exc))
324 317
325 318 return annotations
326 319
327 320 def get_node_type(self, wire, path, revision=None):
328 321
329 322 cache_on, context_uid, repo_id = self._cache_on(wire)
330 323 @self.region.conditional_cache_on_arguments(condition=cache_on)
331 324 def _get_node_type(_repo_id, _path, _revision):
332 325 repo = self._factory.repo(wire)
333 326 fs_ptr = svn.repos.fs(repo)
334 327 if _revision is None:
335 328 _revision = svn.fs.youngest_rev(fs_ptr)
336 329 root = svn.fs.revision_root(fs_ptr, _revision)
337 330 node = svn.fs.check_path(root, path)
338 331 return NODE_TYPE_MAPPING.get(node, None)
339 332 return _get_node_type(repo_id, path, revision)
340 333
341 334 def get_nodes(self, wire, path, revision=None):
342 335
343 336 cache_on, context_uid, repo_id = self._cache_on(wire)
344 337 @self.region.conditional_cache_on_arguments(condition=cache_on)
345 338 def _get_nodes(_repo_id, _path, _revision):
346 339 repo = self._factory.repo(wire)
347 340 fsobj = svn.repos.fs(repo)
348 341 if _revision is None:
349 342 _revision = svn.fs.youngest_rev(fsobj)
350 343 root = svn.fs.revision_root(fsobj, _revision)
351 344 entries = svn.fs.dir_entries(root, path)
352 345 result = []
353 346 for entry_path, entry_info in entries.iteritems():
354 347 result.append(
355 348 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
356 349 return result
357 350 return _get_nodes(repo_id, path, revision)
358 351
359 352 def get_file_content(self, wire, path, rev=None):
360 353 repo = self._factory.repo(wire)
361 354 fsobj = svn.repos.fs(repo)
362 355 if rev is None:
363 356 rev = svn.fs.youngest_revision(fsobj)
364 357 root = svn.fs.revision_root(fsobj, rev)
365 358 content = svn.core.Stream(svn.fs.file_contents(root, path))
366 359 return content.read()
367 360
368 361 def get_file_size(self, wire, path, revision=None):
369 362
370 363 cache_on, context_uid, repo_id = self._cache_on(wire)
371 364 @self.region.conditional_cache_on_arguments(condition=cache_on)
372 365 def _get_file_size(_repo_id, _path, _revision):
373 366 repo = self._factory.repo(wire)
374 367 fsobj = svn.repos.fs(repo)
375 368 if _revision is None:
376 369 _revision = svn.fs.youngest_revision(fsobj)
377 370 root = svn.fs.revision_root(fsobj, _revision)
378 371 size = svn.fs.file_length(root, path)
379 372 return size
380 373 return _get_file_size(repo_id, path, revision)
381 374
382 375 def create_repository(self, wire, compatible_version=None):
383 376 log.info('Creating Subversion repository in path "%s"', wire['path'])
384 377 self._factory.repo(wire, create=True,
385 378 compatible_version=compatible_version)
386 379
387 380 def get_url_and_credentials(self, src_url):
388 381 obj = urlparse.urlparse(src_url)
389 382 username = obj.username or None
390 383 password = obj.password or None
391 384 return username, password, src_url
392 385
393 386 def import_remote_repository(self, wire, src_url):
394 387 repo_path = wire['path']
395 388 if not self.is_path_valid_repository(wire, repo_path):
396 389 raise Exception(
397 390 "Path %s is not a valid Subversion repository." % repo_path)
398 391
399 392 username, password, src_url = self.get_url_and_credentials(src_url)
400 393 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
401 394 '--trust-server-cert-failures=unknown-ca']
402 395 if username and password:
403 396 rdump_cmd += ['--username', username, '--password', password]
404 397 rdump_cmd += [src_url]
405 398
406 399 rdump = subprocess.Popen(
407 400 rdump_cmd,
408 401 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
409 402 load = subprocess.Popen(
410 403 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
411 404
412 405 # TODO: johbo: This can be a very long operation, might be better
413 406 # to track some kind of status and provide an api to check if the
414 407 # import is done.
415 408 rdump.wait()
416 409 load.wait()
417 410
418 411 log.debug('Return process ended with code: %s', rdump.returncode)
419 412 if rdump.returncode != 0:
420 413 errors = rdump.stderr.read()
421 414 log.error('svnrdump dump failed: statuscode %s: message: %s',
422 415 rdump.returncode, errors)
423 416 reason = 'UNKNOWN'
424 417 if 'svnrdump: E230001:' in errors:
425 418 reason = 'INVALID_CERTIFICATE'
426 419
427 420 if reason == 'UNKNOWN':
428 421 reason = 'UNKNOWN:{}'.format(errors)
429 422 raise Exception(
430 423 'Failed to dump the remote repository from %s. Reason:%s' % (
431 424 src_url, reason))
432 425 if load.returncode != 0:
433 426 raise Exception(
434 427 'Failed to load the dump of remote repository from %s.' %
435 428 (src_url, ))
436 429
437 430 def commit(self, wire, message, author, timestamp, updated, removed):
438 431 assert isinstance(message, str)
439 432 assert isinstance(author, str)
440 433
441 434 repo = self._factory.repo(wire)
442 435 fsobj = svn.repos.fs(repo)
443 436
444 437 rev = svn.fs.youngest_rev(fsobj)
445 438 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
446 439 txn_root = svn.fs.txn_root(txn)
447 440
448 441 for node in updated:
449 442 TxnNodeProcessor(node, txn_root).update()
450 443 for node in removed:
451 444 TxnNodeProcessor(node, txn_root).remove()
452 445
453 446 commit_id = svn.repos.fs_commit_txn(repo, txn)
454 447
455 448 if timestamp:
456 449 apr_time = apr_time_t(timestamp)
457 450 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
458 451 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
459 452
460 453 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
461 454 return commit_id
462 455
463 456 def diff(self, wire, rev1, rev2, path1=None, path2=None,
464 457 ignore_whitespace=False, context=3):
465 458
466 459 wire.update(cache=False)
467 460 repo = self._factory.repo(wire)
468 461 diff_creator = SvnDiffer(
469 462 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
470 463 try:
471 464 return diff_creator.generate_diff()
472 465 except svn.core.SubversionException as e:
473 466 log.exception(
474 467 "Error during diff operation operation. "
475 468 "Path might not exist %s, %s" % (path1, path2))
476 469 return ""
477 470
478 471 @reraise_safe_exceptions
479 472 def is_large_file(self, wire, path):
480 473 return False
481 474
482 475 @reraise_safe_exceptions
483 476 def run_svn_command(self, wire, cmd, **opts):
484 477 path = wire.get('path', None)
485 478
486 479 if path and os.path.isdir(path):
487 480 opts['cwd'] = path
488 481
489 482 safe_call = False
490 483 if '_safe' in opts:
491 484 safe_call = True
492 485
493 486 svnenv = os.environ.copy()
494 487 svnenv.update(opts.pop('extra_env', {}))
495 488
496 489 _opts = {'env': svnenv, 'shell': False}
497 490
498 491 try:
499 492 _opts.update(opts)
500 493 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
501 494
502 495 return ''.join(p), ''.join(p.error)
503 496 except (EnvironmentError, OSError) as err:
504 497 cmd = ' '.join(cmd) # human friendly CMD
505 498 tb_err = ("Couldn't run svn command (%s).\n"
506 499 "Original error was:%s\n"
507 500 "Call options:%s\n"
508 501 % (cmd, err, _opts))
509 502 log.exception(tb_err)
510 503 if safe_call:
511 504 return '', err
512 505 else:
513 506 raise exceptions.VcsException()(tb_err)
514 507
515 508 @reraise_safe_exceptions
516 509 def install_hooks(self, wire, force=False):
517 510 from vcsserver.hook_utils import install_svn_hooks
518 511 repo_path = wire['path']
519 512 binary_dir = settings.BINARY_DIR
520 513 executable = None
521 514 if binary_dir:
522 515 executable = os.path.join(binary_dir, 'python')
523 516 return install_svn_hooks(
524 517 repo_path, executable=executable, force_create=force)
525 518
526 519 @reraise_safe_exceptions
527 520 def get_hooks_info(self, wire):
528 521 from vcsserver.hook_utils import (
529 522 get_svn_pre_hook_version, get_svn_post_hook_version)
530 523 repo_path = wire['path']
531 524 return {
532 525 'pre_version': get_svn_pre_hook_version(repo_path),
533 526 'post_version': get_svn_post_hook_version(repo_path),
534 527 }
535 528
536 529
537 530 class SvnDiffer(object):
538 531 """
539 532 Utility to create diffs based on difflib and the Subversion api
540 533 """
541 534
542 535 binary_content = False
543 536
544 537 def __init__(
545 538 self, repo, src_rev, src_path, tgt_rev, tgt_path,
546 539 ignore_whitespace, context):
547 540 self.repo = repo
548 541 self.ignore_whitespace = ignore_whitespace
549 542 self.context = context
550 543
551 544 fsobj = svn.repos.fs(repo)
552 545
553 546 self.tgt_rev = tgt_rev
554 547 self.tgt_path = tgt_path or ''
555 548 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
556 549 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
557 550
558 551 self.src_rev = src_rev
559 552 self.src_path = src_path or self.tgt_path
560 553 self.src_root = svn.fs.revision_root(fsobj, src_rev)
561 554 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
562 555
563 556 self._validate()
564 557
565 558 def _validate(self):
566 559 if (self.tgt_kind != svn.core.svn_node_none and
567 560 self.src_kind != svn.core.svn_node_none and
568 561 self.src_kind != self.tgt_kind):
569 562 # TODO: johbo: proper error handling
570 563 raise Exception(
571 564 "Source and target are not compatible for diff generation. "
572 565 "Source type: %s, target type: %s" %
573 566 (self.src_kind, self.tgt_kind))
574 567
575 568 def generate_diff(self):
576 569 buf = StringIO.StringIO()
577 570 if self.tgt_kind == svn.core.svn_node_dir:
578 571 self._generate_dir_diff(buf)
579 572 else:
580 573 self._generate_file_diff(buf)
581 574 return buf.getvalue()
582 575
583 576 def _generate_dir_diff(self, buf):
584 577 editor = DiffChangeEditor()
585 578 editor_ptr, editor_baton = svn.delta.make_editor(editor)
586 579 svn.repos.dir_delta2(
587 580 self.src_root,
588 581 self.src_path,
589 582 '', # src_entry
590 583 self.tgt_root,
591 584 self.tgt_path,
592 585 editor_ptr, editor_baton,
593 586 authorization_callback_allow_all,
594 587 False, # text_deltas
595 588 svn.core.svn_depth_infinity, # depth
596 589 False, # entry_props
597 590 False, # ignore_ancestry
598 591 )
599 592
600 593 for path, __, change in sorted(editor.changes):
601 594 self._generate_node_diff(
602 595 buf, change, path, self.tgt_path, path, self.src_path)
603 596
604 597 def _generate_file_diff(self, buf):
605 598 change = None
606 599 if self.src_kind == svn.core.svn_node_none:
607 600 change = "add"
608 601 elif self.tgt_kind == svn.core.svn_node_none:
609 602 change = "delete"
610 603 tgt_base, tgt_path = vcspath.split(self.tgt_path)
611 604 src_base, src_path = vcspath.split(self.src_path)
612 605 self._generate_node_diff(
613 606 buf, change, tgt_path, tgt_base, src_path, src_base)
614 607
615 608 def _generate_node_diff(
616 609 self, buf, change, tgt_path, tgt_base, src_path, src_base):
617 610
618 611 if self.src_rev == self.tgt_rev and tgt_base == src_base:
619 612 # makes consistent behaviour with git/hg to return empty diff if
620 613 # we compare same revisions
621 614 return
622 615
623 616 tgt_full_path = vcspath.join(tgt_base, tgt_path)
624 617 src_full_path = vcspath.join(src_base, src_path)
625 618
626 619 self.binary_content = False
627 620 mime_type = self._get_mime_type(tgt_full_path)
628 621
629 622 if mime_type and not mime_type.startswith('text'):
630 623 self.binary_content = True
631 624 buf.write("=" * 67 + '\n')
632 625 buf.write("Cannot display: file marked as a binary type.\n")
633 626 buf.write("svn:mime-type = %s\n" % mime_type)
634 627 buf.write("Index: %s\n" % (tgt_path, ))
635 628 buf.write("=" * 67 + '\n')
636 629 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
637 630 'tgt_path': tgt_path})
638 631
639 632 if change == 'add':
640 633 # TODO: johbo: SVN is missing a zero here compared to git
641 634 buf.write("new file mode 10644\n")
642 635
643 636 #TODO(marcink): intro to binary detection of svn patches
644 637 # if self.binary_content:
645 638 # buf.write('GIT binary patch\n')
646 639
647 640 buf.write("--- /dev/null\t(revision 0)\n")
648 641 src_lines = []
649 642 else:
650 643 if change == 'delete':
651 644 buf.write("deleted file mode 10644\n")
652 645
653 646 #TODO(marcink): intro to binary detection of svn patches
654 647 # if self.binary_content:
655 648 # buf.write('GIT binary patch\n')
656 649
657 650 buf.write("--- a/%s\t(revision %s)\n" % (
658 651 src_path, self.src_rev))
659 652 src_lines = self._svn_readlines(self.src_root, src_full_path)
660 653
661 654 if change == 'delete':
662 655 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
663 656 tgt_lines = []
664 657 else:
665 658 buf.write("+++ b/%s\t(revision %s)\n" % (
666 659 tgt_path, self.tgt_rev))
667 660 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
668 661
669 662 if not self.binary_content:
670 663 udiff = svn_diff.unified_diff(
671 664 src_lines, tgt_lines, context=self.context,
672 665 ignore_blank_lines=self.ignore_whitespace,
673 666 ignore_case=False,
674 667 ignore_space_changes=self.ignore_whitespace)
675 668 buf.writelines(udiff)
676 669
677 670 def _get_mime_type(self, path):
678 671 try:
679 672 mime_type = svn.fs.node_prop(
680 673 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
681 674 except svn.core.SubversionException:
682 675 mime_type = svn.fs.node_prop(
683 676 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
684 677 return mime_type
685 678
686 679 def _svn_readlines(self, fs_root, node_path):
687 680 if self.binary_content:
688 681 return []
689 682 node_kind = svn.fs.check_path(fs_root, node_path)
690 683 if node_kind not in (
691 684 svn.core.svn_node_file, svn.core.svn_node_symlink):
692 685 return []
693 686 content = svn.core.Stream(
694 687 svn.fs.file_contents(fs_root, node_path)).read()
695 688 return content.splitlines(True)
696 689
697 690
698 691 class DiffChangeEditor(svn.delta.Editor):
699 692 """
700 693 Records changes between two given revisions
701 694 """
702 695
703 696 def __init__(self):
704 697 self.changes = []
705 698
706 699 def delete_entry(self, path, revision, parent_baton, pool=None):
707 700 self.changes.append((path, None, 'delete'))
708 701
709 702 def add_file(
710 703 self, path, parent_baton, copyfrom_path, copyfrom_revision,
711 704 file_pool=None):
712 705 self.changes.append((path, 'file', 'add'))
713 706
714 707 def open_file(self, path, parent_baton, base_revision, file_pool=None):
715 708 self.changes.append((path, 'file', 'change'))
716 709
717 710
718 711 def authorization_callback_allow_all(root, path, pool):
719 712 return True
720 713
721 714
722 715 class TxnNodeProcessor(object):
723 716 """
724 717 Utility to process the change of one node within a transaction root.
725 718
726 719 It encapsulates the knowledge of how to add, update or remove
727 720 a node for a given transaction root. The purpose is to support the method
728 721 `SvnRemote.commit`.
729 722 """
730 723
731 724 def __init__(self, node, txn_root):
732 725 assert isinstance(node['path'], str)
733 726
734 727 self.node = node
735 728 self.txn_root = txn_root
736 729
737 730 def update(self):
738 731 self._ensure_parent_dirs()
739 732 self._add_file_if_node_does_not_exist()
740 733 self._update_file_content()
741 734 self._update_file_properties()
742 735
743 736 def remove(self):
744 737 svn.fs.delete(self.txn_root, self.node['path'])
745 738 # TODO: Clean up directory if empty
746 739
747 740 def _ensure_parent_dirs(self):
748 741 curdir = vcspath.dirname(self.node['path'])
749 742 dirs_to_create = []
750 743 while not self._svn_path_exists(curdir):
751 744 dirs_to_create.append(curdir)
752 745 curdir = vcspath.dirname(curdir)
753 746
754 747 for curdir in reversed(dirs_to_create):
755 748 log.debug('Creating missing directory "%s"', curdir)
756 749 svn.fs.make_dir(self.txn_root, curdir)
757 750
758 751 def _svn_path_exists(self, path):
759 752 path_status = svn.fs.check_path(self.txn_root, path)
760 753 return path_status != svn.core.svn_node_none
761 754
762 755 def _add_file_if_node_does_not_exist(self):
763 756 kind = svn.fs.check_path(self.txn_root, self.node['path'])
764 757 if kind == svn.core.svn_node_none:
765 758 svn.fs.make_file(self.txn_root, self.node['path'])
766 759
767 760 def _update_file_content(self):
768 761 assert isinstance(self.node['content'], str)
769 762 handler, baton = svn.fs.apply_textdelta(
770 763 self.txn_root, self.node['path'], None, None)
771 764 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
772 765
773 766 def _update_file_properties(self):
774 767 properties = self.node.get('properties', {})
775 768 for key, value in properties.iteritems():
776 769 svn.fs.change_node_prop(
777 770 self.txn_root, self.node['path'], key, value)
778 771
779 772
780 773 def apr_time_t(timestamp):
781 774 """
782 775 Convert a Python timestamp into APR timestamp type apr_time_t
783 776 """
784 777 return timestamp * 1E6
785 778
786 779
787 780 def svn_opt_revision_value_t(num):
788 781 """
789 782 Put `num` into a `svn_opt_revision_value_t` structure.
790 783 """
791 784 value = svn.core.svn_opt_revision_value_t()
792 785 value.number = num
793 786 revision = svn.core.svn_opt_revision_t()
794 787 revision.kind = svn.core.svn_opt_revision_number
795 788 revision.value = value
796 789 return revision
General Comments 0
You need to be logged in to leave comments. Login now