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