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