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