##// END OF EJS Templates
git: speed up extraction of git children call...
super-admin -
r1071:f2aec1d6 python3
parent child Browse files
Show More
@@ -1,1327 +1,1332 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import collections
19 19 import logging
20 20 import os
21 21 import posixpath as vcspath
22 22 import re
23 23 import stat
24 24 import traceback
25 25 import urllib.request, urllib.parse, urllib.error
26 26 import urllib.request, urllib.error, urllib.parse
27 27 from functools import wraps
28 28
29 29 import more_itertools
30 30 import pygit2
31 31 from pygit2 import Repository as LibGit2Repo
32 32 from pygit2 import index as LibGit2Index
33 33 from dulwich import index, objects
34 34 from dulwich.client import HttpGitClient, LocalGitClient
35 35 from dulwich.errors import (
36 36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 37 MissingCommitError, ObjectMissing, HangupException,
38 38 UnexpectedCommandError)
39 39 from dulwich.repo import Repo as DulwichRepo
40 40 from dulwich.server import update_server_info
41 41
42 42 from vcsserver import exceptions, settings, subprocessio
43 43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes
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 = b'^{}'
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def reraise_safe_exceptions(func):
60 60 """Converts Dulwich exceptions to something neutral."""
61 61
62 62 @wraps(func)
63 63 def wrapper(*args, **kwargs):
64 64 try:
65 65 return func(*args, **kwargs)
66 66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 67 exc = exceptions.LookupException(org_exc=e)
68 68 raise exc(safe_str(e))
69 69 except (HangupException, UnexpectedCommandError) as e:
70 70 exc = exceptions.VcsException(org_exc=e)
71 71 raise exc(safe_str(e))
72 72 except Exception as e:
73 73 # NOTE(marcink): becuase of how dulwich handles some exceptions
74 74 # (KeyError on empty repos), we cannot track this and catch all
75 75 # exceptions, it's an exceptions from other handlers
76 76 #if not hasattr(e, '_vcs_kind'):
77 77 #log.exception("Unhandled exception in git remote call")
78 78 #raise_from_original(exceptions.UnhandledException)
79 79 raise
80 80 return wrapper
81 81
82 82
83 83 class Repo(DulwichRepo):
84 84 """
85 85 A wrapper for dulwich Repo class.
86 86
87 87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 88 "Too many open files" error. We need to close all opened file descriptors
89 89 once the repo object is destroyed.
90 90 """
91 91 def __del__(self):
92 92 if hasattr(self, 'object_store'):
93 93 self.close()
94 94
95 95
96 96 class Repository(LibGit2Repo):
97 97
98 98 def __enter__(self):
99 99 return self
100 100
101 101 def __exit__(self, exc_type, exc_val, exc_tb):
102 102 self.free()
103 103
104 104
105 105 class GitFactory(RepoFactory):
106 106 repo_type = 'git'
107 107
108 108 def _create_repo(self, wire, create, use_libgit2=False):
109 109 if use_libgit2:
110 110 return Repository(wire['path'])
111 111 else:
112 112 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
113 113 return Repo(repo_path)
114 114
115 115 def repo(self, wire, create=False, use_libgit2=False):
116 116 """
117 117 Get a repository instance for the given path.
118 118 """
119 119 return self._create_repo(wire, create, use_libgit2)
120 120
121 121 def repo_libgit2(self, wire):
122 122 return self.repo(wire, use_libgit2=True)
123 123
124 124
125 125 class GitRemote(RemoteBase):
126 126
127 127 def __init__(self, factory):
128 128 self._factory = factory
129 129 self._bulk_methods = {
130 130 "date": self.date,
131 131 "author": self.author,
132 132 "branch": self.branch,
133 133 "message": self.message,
134 134 "parents": self.parents,
135 135 "_commit": self.revision,
136 136 }
137 137
138 138 def _wire_to_config(self, wire):
139 139 if 'config' in wire:
140 140 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
141 141 return {}
142 142
143 143 def _remote_conf(self, config):
144 144 params = [
145 145 '-c', 'core.askpass=""',
146 146 ]
147 147 ssl_cert_dir = config.get('vcs_ssl_dir')
148 148 if ssl_cert_dir:
149 149 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
150 150 return params
151 151
152 152 @reraise_safe_exceptions
153 153 def discover_git_version(self):
154 154 stdout, _ = self.run_git_command(
155 155 {}, ['--version'], _bare=True, _safe=True)
156 156 prefix = b'git version'
157 157 if stdout.startswith(prefix):
158 158 stdout = stdout[len(prefix):]
159 159 return safe_str(stdout.strip())
160 160
161 161 @reraise_safe_exceptions
162 162 def is_empty(self, wire):
163 163 repo_init = self._factory.repo_libgit2(wire)
164 164 with repo_init as repo:
165 165
166 166 try:
167 167 has_head = repo.head.name
168 168 if has_head:
169 169 return False
170 170
171 171 # NOTE(marcink): check again using more expensive method
172 172 return repo.is_empty
173 173 except Exception:
174 174 pass
175 175
176 176 return True
177 177
178 178 @reraise_safe_exceptions
179 179 def assert_correct_path(self, wire):
180 180 cache_on, context_uid, repo_id = self._cache_on(wire)
181 181 region = self._region(wire)
182 182
183 183 @region.conditional_cache_on_arguments(condition=cache_on)
184 184 def _assert_correct_path(_context_uid, _repo_id):
185 185 try:
186 186 repo_init = self._factory.repo_libgit2(wire)
187 187 with repo_init as repo:
188 188 pass
189 189 except pygit2.GitError:
190 190 path = wire.get('path')
191 191 tb = traceback.format_exc()
192 192 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
193 193 return False
194 194
195 195 return True
196 196 return _assert_correct_path(context_uid, repo_id)
197 197
198 198 @reraise_safe_exceptions
199 199 def bare(self, wire):
200 200 repo_init = self._factory.repo_libgit2(wire)
201 201 with repo_init as repo:
202 202 return repo.is_bare
203 203
204 204 @reraise_safe_exceptions
205 205 def blob_as_pretty_string(self, wire, sha):
206 206 repo_init = self._factory.repo_libgit2(wire)
207 207 with repo_init as repo:
208 208 blob_obj = repo[sha]
209 209 blob = blob_obj.data
210 210 return blob
211 211
212 212 @reraise_safe_exceptions
213 213 def blob_raw_length(self, wire, sha):
214 214 cache_on, context_uid, repo_id = self._cache_on(wire)
215 215 region = self._region(wire)
216 216
217 217 @region.conditional_cache_on_arguments(condition=cache_on)
218 218 def _blob_raw_length(_repo_id, _sha):
219 219
220 220 repo_init = self._factory.repo_libgit2(wire)
221 221 with repo_init as repo:
222 222 blob = repo[sha]
223 223 return blob.size
224 224
225 225 return _blob_raw_length(repo_id, sha)
226 226
227 227 def _parse_lfs_pointer(self, raw_content):
228 228 spec_string = b'version https://git-lfs.github.com/spec'
229 229 if raw_content and raw_content.startswith(spec_string):
230 230
231 231 pattern = re.compile(rb"""
232 232 (?:\n)?
233 233 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
234 234 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
235 235 ^size[ ](?P<oid_size>[0-9]+)\n
236 236 (?:\n)?
237 237 """, re.VERBOSE | re.MULTILINE)
238 238 match = pattern.match(raw_content)
239 239 if match:
240 240 return match.groupdict()
241 241
242 242 return {}
243 243
244 244 @reraise_safe_exceptions
245 245 def is_large_file(self, wire, commit_id):
246 246 cache_on, context_uid, repo_id = self._cache_on(wire)
247 247 region = self._region(wire)
248 248
249 249 @region.conditional_cache_on_arguments(condition=cache_on)
250 250 def _is_large_file(_repo_id, _sha):
251 251 repo_init = self._factory.repo_libgit2(wire)
252 252 with repo_init as repo:
253 253 blob = repo[commit_id]
254 254 if blob.is_binary:
255 255 return {}
256 256
257 257 return self._parse_lfs_pointer(blob.data)
258 258
259 259 return _is_large_file(repo_id, commit_id)
260 260
261 261 @reraise_safe_exceptions
262 262 def is_binary(self, wire, tree_id):
263 263 cache_on, context_uid, repo_id = self._cache_on(wire)
264 264 region = self._region(wire)
265 265
266 266 @region.conditional_cache_on_arguments(condition=cache_on)
267 267 def _is_binary(_repo_id, _tree_id):
268 268 repo_init = self._factory.repo_libgit2(wire)
269 269 with repo_init as repo:
270 270 blob_obj = repo[tree_id]
271 271 return blob_obj.is_binary
272 272
273 273 return _is_binary(repo_id, tree_id)
274 274
275 275 @reraise_safe_exceptions
276 276 def in_largefiles_store(self, wire, oid):
277 277 conf = self._wire_to_config(wire)
278 278 repo_init = self._factory.repo_libgit2(wire)
279 279 with repo_init as repo:
280 280 repo_name = repo.path
281 281
282 282 store_location = conf.get('vcs_git_lfs_store_location')
283 283 if store_location:
284 284
285 285 store = LFSOidStore(
286 286 oid=oid, repo=repo_name, store_location=store_location)
287 287 return store.has_oid()
288 288
289 289 return False
290 290
291 291 @reraise_safe_exceptions
292 292 def store_path(self, wire, oid):
293 293 conf = self._wire_to_config(wire)
294 294 repo_init = self._factory.repo_libgit2(wire)
295 295 with repo_init as repo:
296 296 repo_name = repo.path
297 297
298 298 store_location = conf.get('vcs_git_lfs_store_location')
299 299 if store_location:
300 300 store = LFSOidStore(
301 301 oid=oid, repo=repo_name, store_location=store_location)
302 302 return store.oid_path
303 303 raise ValueError('Unable to fetch oid with path {}'.format(oid))
304 304
305 305 @reraise_safe_exceptions
306 306 def bulk_request(self, wire, rev, pre_load):
307 307 cache_on, context_uid, repo_id = self._cache_on(wire)
308 308 region = self._region(wire)
309 309
310 310 @region.conditional_cache_on_arguments(condition=cache_on)
311 311 def _bulk_request(_repo_id, _rev, _pre_load):
312 312 result = {}
313 313 for attr in pre_load:
314 314 try:
315 315 method = self._bulk_methods[attr]
316 316 args = [wire, rev]
317 317 result[attr] = method(*args)
318 318 except KeyError as e:
319 319 raise exceptions.VcsException(e)(
320 320 "Unknown bulk attribute: %s" % attr)
321 321 return result
322 322
323 323 return _bulk_request(repo_id, rev, sorted(pre_load))
324 324
325 325 def _build_opener(self, url):
326 326 handlers = []
327 327 url_obj = url_parser(url)
328 328 _, authinfo = url_obj.authinfo()
329 329
330 330 if authinfo:
331 331 # create a password manager
332 332 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
333 333 passmgr.add_password(*authinfo)
334 334
335 335 handlers.extend((httpbasicauthhandler(passmgr),
336 336 httpdigestauthhandler(passmgr)))
337 337
338 338 return urllib.request.build_opener(*handlers)
339 339
340 340 def _type_id_to_name(self, type_id: int):
341 341 return {
342 342 1: 'commit',
343 343 2: 'tree',
344 344 3: 'blob',
345 345 4: 'tag'
346 346 }[type_id]
347 347
348 348 @reraise_safe_exceptions
349 349 def check_url(self, url, config):
350 350 url_obj = url_parser(url)
351 351 test_uri, _ = url_obj.authinfo()
352 352 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
353 353 url_obj.query = obfuscate_qs(url_obj.query)
354 354 cleaned_uri = str(url_obj)
355 355 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
356 356
357 357 if not test_uri.endswith('info/refs'):
358 358 test_uri = test_uri.rstrip('/') + '/info/refs'
359 359
360 360 o = self._build_opener(url)
361 361 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
362 362
363 363 q = {"service": 'git-upload-pack'}
364 364 qs = '?%s' % urllib.parse.urlencode(q)
365 365 cu = "%s%s" % (test_uri, qs)
366 366 req = urllib.request.Request(cu, None, {})
367 367
368 368 try:
369 369 log.debug("Trying to open URL %s", cleaned_uri)
370 370 resp = o.open(req)
371 371 if resp.code != 200:
372 372 raise exceptions.URLError()('Return Code is not 200')
373 373 except Exception as e:
374 374 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
375 375 # means it cannot be cloned
376 376 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
377 377
378 378 # now detect if it's proper git repo
379 379 gitdata = resp.read()
380 380 if 'service=git-upload-pack' in gitdata:
381 381 pass
382 382 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
383 383 # old style git can return some other format !
384 384 pass
385 385 else:
386 386 raise exceptions.URLError()(
387 387 "url [%s] does not look like an git" % (cleaned_uri,))
388 388
389 389 return True
390 390
391 391 @reraise_safe_exceptions
392 392 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
393 393 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
394 394 remote_refs = self.pull(wire, url, apply_refs=False)
395 395 repo = self._factory.repo(wire)
396 396 if isinstance(valid_refs, list):
397 397 valid_refs = tuple(valid_refs)
398 398
399 399 for k in remote_refs:
400 400 # only parse heads/tags and skip so called deferred tags
401 401 if k.startswith(valid_refs) and not k.endswith(deferred):
402 402 repo[k] = remote_refs[k]
403 403
404 404 if update_after_clone:
405 405 # we want to checkout HEAD
406 406 repo["HEAD"] = remote_refs["HEAD"]
407 407 index.build_index_from_tree(repo.path, repo.index_path(),
408 408 repo.object_store, repo["HEAD"].tree)
409 409
410 410 @reraise_safe_exceptions
411 411 def branch(self, wire, commit_id):
412 412 cache_on, context_uid, repo_id = self._cache_on(wire)
413 413 region = self._region(wire)
414 414 @region.conditional_cache_on_arguments(condition=cache_on)
415 415 def _branch(_context_uid, _repo_id, _commit_id):
416 416 regex = re.compile('^refs/heads')
417 417
418 418 def filter_with(ref):
419 419 return regex.match(ref[0]) and ref[1] == _commit_id
420 420
421 421 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
422 422 return [x[0].split('refs/heads/')[-1] for x in branches]
423 423
424 424 return _branch(context_uid, repo_id, commit_id)
425 425
426 426 @reraise_safe_exceptions
427 427 def commit_branches(self, wire, commit_id):
428 428 cache_on, context_uid, repo_id = self._cache_on(wire)
429 429 region = self._region(wire)
430 430 @region.conditional_cache_on_arguments(condition=cache_on)
431 431 def _commit_branches(_context_uid, _repo_id, _commit_id):
432 432 repo_init = self._factory.repo_libgit2(wire)
433 433 with repo_init as repo:
434 434 branches = [x for x in repo.branches.with_commit(_commit_id)]
435 435 return branches
436 436
437 437 return _commit_branches(context_uid, repo_id, commit_id)
438 438
439 439 @reraise_safe_exceptions
440 440 def add_object(self, wire, content):
441 441 repo_init = self._factory.repo_libgit2(wire)
442 442 with repo_init as repo:
443 443 blob = objects.Blob()
444 444 blob.set_raw_string(content)
445 445 repo.object_store.add_object(blob)
446 446 return blob.id
447 447
448 448 # TODO: this is quite complex, check if that can be simplified
449 449 @reraise_safe_exceptions
450 450 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
451 451 # Defines the root tree
452 452 class _Root(object):
453 453 def __repr__(self):
454 454 return 'ROOT TREE'
455 455 ROOT = _Root()
456 456
457 457 repo = self._factory.repo(wire)
458 458 object_store = repo.object_store
459 459
460 460 # Create tree and populates it with blobs
461 461
462 462 if commit_tree and repo[commit_tree]:
463 463 git_commit = repo[commit_data['parents'][0]]
464 464 commit_tree = repo[git_commit.tree] # root tree
465 465 else:
466 466 commit_tree = objects.Tree()
467 467
468 468 for node in updated:
469 469 # Compute subdirs if needed
470 470 dirpath, nodename = vcspath.split(node['path'])
471 471 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
472 472 parent = commit_tree
473 473 ancestors = [('', parent)]
474 474
475 475 # Tries to dig for the deepest existing tree
476 476 while dirnames:
477 477 curdir = dirnames.pop(0)
478 478 try:
479 479 dir_id = parent[curdir][1]
480 480 except KeyError:
481 481 # put curdir back into dirnames and stops
482 482 dirnames.insert(0, curdir)
483 483 break
484 484 else:
485 485 # If found, updates parent
486 486 parent = repo[dir_id]
487 487 ancestors.append((curdir, parent))
488 488 # Now parent is deepest existing tree and we need to create
489 489 # subtrees for dirnames (in reverse order)
490 490 # [this only applies for nodes from added]
491 491 new_trees = []
492 492
493 493 blob = objects.Blob.from_string(node['content'])
494 494
495 495 if dirnames:
496 496 # If there are trees which should be created we need to build
497 497 # them now (in reverse order)
498 498 reversed_dirnames = list(reversed(dirnames))
499 499 curtree = objects.Tree()
500 500 curtree[node['node_path']] = node['mode'], blob.id
501 501 new_trees.append(curtree)
502 502 for dirname in reversed_dirnames[:-1]:
503 503 newtree = objects.Tree()
504 504 newtree[dirname] = (DIR_STAT, curtree.id)
505 505 new_trees.append(newtree)
506 506 curtree = newtree
507 507 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
508 508 else:
509 509 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
510 510
511 511 new_trees.append(parent)
512 512 # Update ancestors
513 513 reversed_ancestors = reversed(
514 514 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
515 515 for parent, tree, path in reversed_ancestors:
516 516 parent[path] = (DIR_STAT, tree.id)
517 517 object_store.add_object(tree)
518 518
519 519 object_store.add_object(blob)
520 520 for tree in new_trees:
521 521 object_store.add_object(tree)
522 522
523 523 for node_path in removed:
524 524 paths = node_path.split('/')
525 525 tree = commit_tree # start with top-level
526 526 trees = [{'tree': tree, 'path': ROOT}]
527 527 # Traverse deep into the forest...
528 528 # resolve final tree by iterating the path.
529 529 # e.g a/b/c.txt will get
530 530 # - root as tree then
531 531 # - 'a' as tree,
532 532 # - 'b' as tree,
533 533 # - stop at c as blob.
534 534 for path in paths:
535 535 try:
536 536 obj = repo[tree[path][1]]
537 537 if isinstance(obj, objects.Tree):
538 538 trees.append({'tree': obj, 'path': path})
539 539 tree = obj
540 540 except KeyError:
541 541 break
542 542 #PROBLEM:
543 543 """
544 544 We're not editing same reference tree object
545 545 """
546 546 # Cut down the blob and all rotten trees on the way back...
547 547 for path, tree_data in reversed(list(zip(paths, trees))):
548 548 tree = tree_data['tree']
549 549 tree.__delitem__(path)
550 550 # This operation edits the tree, we need to mark new commit back
551 551
552 552 if len(tree) > 0:
553 553 # This tree still has elements - don't remove it or any
554 554 # of it's parents
555 555 break
556 556
557 557 object_store.add_object(commit_tree)
558 558
559 559 # Create commit
560 560 commit = objects.Commit()
561 561 commit.tree = commit_tree.id
562 562 bytes_keys = [
563 563 'author',
564 564 'committer',
565 565 'message',
566 566 'encoding'
567 567 ]
568 568
569 569 for k, v in commit_data.items():
570 570 if k in bytes_keys:
571 571 v = safe_bytes(v)
572 572 setattr(commit, k, v)
573 573
574 574 object_store.add_object(commit)
575 575
576 576 self.create_branch(wire, branch, safe_str(commit.id))
577 577
578 578 # dulwich set-ref
579 579 repo.refs[safe_bytes(f'refs/heads/{branch}')] = commit.id
580 580
581 581 return commit.id
582 582
583 583 @reraise_safe_exceptions
584 584 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
585 585 if url != 'default' and '://' not in url:
586 586 client = LocalGitClient(url)
587 587 else:
588 588 url_obj = url_parser(url)
589 589 o = self._build_opener(url)
590 590 url, _ = url_obj.authinfo()
591 591 client = HttpGitClient(base_url=url, opener=o)
592 592 repo = self._factory.repo(wire)
593 593
594 594 determine_wants = repo.object_store.determine_wants_all
595 595 if refs:
596 596 def determine_wants_requested(references):
597 597 return [references[r] for r in references if r in refs]
598 598 determine_wants = determine_wants_requested
599 599
600 600 try:
601 601 remote_refs = client.fetch(
602 602 path=url, target=repo, determine_wants=determine_wants)
603 603 except NotGitRepository as e:
604 604 log.warning(
605 605 'Trying to fetch from "%s" failed, not a Git repository.', url)
606 606 # Exception can contain unicode which we convert
607 607 raise exceptions.AbortException(e)(repr(e))
608 608
609 609 # mikhail: client.fetch() returns all the remote refs, but fetches only
610 610 # refs filtered by `determine_wants` function. We need to filter result
611 611 # as well
612 612 if refs:
613 613 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
614 614
615 615 if apply_refs:
616 616 # TODO: johbo: Needs proper test coverage with a git repository
617 617 # that contains a tag object, so that we would end up with
618 618 # a peeled ref at this point.
619 619 for k in remote_refs:
620 620 if k.endswith(PEELED_REF_MARKER):
621 621 log.debug("Skipping peeled reference %s", k)
622 622 continue
623 623 repo[k] = remote_refs[k]
624 624
625 625 if refs and not update_after:
626 626 # mikhail: explicitly set the head to the last ref.
627 627 repo["HEAD"] = remote_refs[refs[-1]]
628 628
629 629 if update_after:
630 630 # we want to checkout HEAD
631 631 repo["HEAD"] = remote_refs["HEAD"]
632 632 index.build_index_from_tree(repo.path, repo.index_path(),
633 633 repo.object_store, repo["HEAD"].tree)
634 634 return remote_refs
635 635
636 636 @reraise_safe_exceptions
637 637 def sync_fetch(self, wire, url, refs=None, all_refs=False):
638 638 repo = self._factory.repo(wire)
639 639 if refs and not isinstance(refs, (list, tuple)):
640 640 refs = [refs]
641 641
642 642 config = self._wire_to_config(wire)
643 643 # get all remote refs we'll use to fetch later
644 644 cmd = ['ls-remote']
645 645 if not all_refs:
646 646 cmd += ['--heads', '--tags']
647 647 cmd += [url]
648 648 output, __ = self.run_git_command(
649 649 wire, cmd, fail_on_stderr=False,
650 650 _copts=self._remote_conf(config),
651 651 extra_env={'GIT_TERMINAL_PROMPT': '0'})
652 652
653 653 remote_refs = collections.OrderedDict()
654 654 fetch_refs = []
655 655
656 656 for ref_line in output.splitlines():
657 657 sha, ref = ref_line.split(b'\t')
658 658 sha = sha.strip()
659 659 if ref in remote_refs:
660 660 # duplicate, skip
661 661 continue
662 662 if ref.endswith(PEELED_REF_MARKER):
663 663 log.debug("Skipping peeled reference %s", ref)
664 664 continue
665 665 # don't sync HEAD
666 666 if ref in [b'HEAD']:
667 667 continue
668 668
669 669 remote_refs[ref] = sha
670 670
671 671 if refs and sha in refs:
672 672 # we filter fetch using our specified refs
673 673 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
674 674 elif not refs:
675 675 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
676 676 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
677 677
678 678 if fetch_refs:
679 679 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
680 680 fetch_refs_chunks = list(chunk)
681 681 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
682 682 self.run_git_command(
683 683 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
684 684 fail_on_stderr=False,
685 685 _copts=self._remote_conf(config),
686 686 extra_env={'GIT_TERMINAL_PROMPT': '0'})
687 687
688 688 return remote_refs
689 689
690 690 @reraise_safe_exceptions
691 691 def sync_push(self, wire, url, refs=None):
692 692 if not self.check_url(url, wire):
693 693 return
694 694 config = self._wire_to_config(wire)
695 695 self._factory.repo(wire)
696 696 self.run_git_command(
697 697 wire, ['push', url, '--mirror'], fail_on_stderr=False,
698 698 _copts=self._remote_conf(config),
699 699 extra_env={'GIT_TERMINAL_PROMPT': '0'})
700 700
701 701 @reraise_safe_exceptions
702 702 def get_remote_refs(self, wire, url):
703 703 repo = Repo(url)
704 704 return repo.get_refs()
705 705
706 706 @reraise_safe_exceptions
707 707 def get_description(self, wire):
708 708 repo = self._factory.repo(wire)
709 709 return repo.get_description()
710 710
711 711 @reraise_safe_exceptions
712 712 def get_missing_revs(self, wire, rev1, rev2, path2):
713 713 repo = self._factory.repo(wire)
714 714 LocalGitClient(thin_packs=False).fetch(path2, repo)
715 715
716 716 wire_remote = wire.copy()
717 717 wire_remote['path'] = path2
718 718 repo_remote = self._factory.repo(wire_remote)
719 719 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
720 720
721 721 revs = [
722 722 x.commit.id
723 723 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
724 724 return revs
725 725
726 726 @reraise_safe_exceptions
727 727 def get_object(self, wire, sha, maybe_unreachable=False):
728 728 cache_on, context_uid, repo_id = self._cache_on(wire)
729 729 region = self._region(wire)
730 730
731 731 @region.conditional_cache_on_arguments(condition=cache_on)
732 732 def _get_object(_context_uid, _repo_id, _sha):
733 733 repo_init = self._factory.repo_libgit2(wire)
734 734 with repo_init as repo:
735 735
736 736 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
737 737 try:
738 738 commit = repo.revparse_single(sha)
739 739 except KeyError:
740 740 # NOTE(marcink): KeyError doesn't give us any meaningful information
741 741 # here, we instead give something more explicit
742 742 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
743 743 raise exceptions.LookupException(e)(missing_commit_err)
744 744 except ValueError as e:
745 745 raise exceptions.LookupException(e)(missing_commit_err)
746 746
747 747 is_tag = False
748 748 if isinstance(commit, pygit2.Tag):
749 749 commit = repo.get(commit.target)
750 750 is_tag = True
751 751
752 752 check_dangling = True
753 753 if is_tag:
754 754 check_dangling = False
755 755
756 756 if check_dangling and maybe_unreachable:
757 757 check_dangling = False
758 758
759 759 # we used a reference and it parsed means we're not having a dangling commit
760 760 if sha != commit.hex:
761 761 check_dangling = False
762 762
763 763 if check_dangling:
764 764 # check for dangling commit
765 765 for branch in repo.branches.with_commit(commit.hex):
766 766 if branch:
767 767 break
768 768 else:
769 769 # NOTE(marcink): Empty error doesn't give us any meaningful information
770 770 # here, we instead give something more explicit
771 771 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
772 772 raise exceptions.LookupException(e)(missing_commit_err)
773 773
774 774 commit_id = commit.hex
775 775 type_id = commit.type
776 776
777 777 return {
778 778 'id': commit_id,
779 779 'type': self._type_id_to_name(type_id),
780 780 'commit_id': commit_id,
781 781 'idx': 0
782 782 }
783 783
784 784 return _get_object(context_uid, repo_id, sha)
785 785
786 786 @reraise_safe_exceptions
787 787 def get_refs(self, wire):
788 788 cache_on, context_uid, repo_id = self._cache_on(wire)
789 789 region = self._region(wire)
790 790
791 791 @region.conditional_cache_on_arguments(condition=cache_on)
792 792 def _get_refs(_context_uid, _repo_id):
793 793
794 794 repo_init = self._factory.repo_libgit2(wire)
795 795 with repo_init as repo:
796 796 regex = re.compile('^refs/(heads|tags)/')
797 797 return {x.name: x.target.hex for x in
798 798 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
799 799
800 800 return _get_refs(context_uid, repo_id)
801 801
802 802 @reraise_safe_exceptions
803 803 def get_branch_pointers(self, wire):
804 804 cache_on, context_uid, repo_id = self._cache_on(wire)
805 805 region = self._region(wire)
806 806
807 807 @region.conditional_cache_on_arguments(condition=cache_on)
808 808 def _get_branch_pointers(_context_uid, _repo_id):
809 809
810 810 repo_init = self._factory.repo_libgit2(wire)
811 811 regex = re.compile('^refs/heads')
812 812 with repo_init as repo:
813 813 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
814 814 return {x.target.hex: x.shorthand for x in branches}
815 815
816 816 return _get_branch_pointers(context_uid, repo_id)
817 817
818 818 @reraise_safe_exceptions
819 819 def head(self, wire, show_exc=True):
820 820 cache_on, context_uid, repo_id = self._cache_on(wire)
821 821 region = self._region(wire)
822 822
823 823 @region.conditional_cache_on_arguments(condition=cache_on)
824 824 def _head(_context_uid, _repo_id, _show_exc):
825 825 repo_init = self._factory.repo_libgit2(wire)
826 826 with repo_init as repo:
827 827 try:
828 828 return repo.head.peel().hex
829 829 except Exception:
830 830 if show_exc:
831 831 raise
832 832 return _head(context_uid, repo_id, show_exc)
833 833
834 834 @reraise_safe_exceptions
835 835 def init(self, wire):
836 836 repo_path = safe_str(wire['path'])
837 837 self.repo = Repo.init(repo_path)
838 838
839 839 @reraise_safe_exceptions
840 840 def init_bare(self, wire):
841 841 repo_path = safe_str(wire['path'])
842 842 self.repo = Repo.init_bare(repo_path)
843 843
844 844 @reraise_safe_exceptions
845 845 def revision(self, wire, rev):
846 846
847 847 cache_on, context_uid, repo_id = self._cache_on(wire)
848 848 region = self._region(wire)
849 849
850 850 @region.conditional_cache_on_arguments(condition=cache_on)
851 851 def _revision(_context_uid, _repo_id, _rev):
852 852 repo_init = self._factory.repo_libgit2(wire)
853 853 with repo_init as repo:
854 854 commit = repo[rev]
855 855 obj_data = {
856 856 'id': commit.id.hex,
857 857 }
858 858 # tree objects itself don't have tree_id attribute
859 859 if hasattr(commit, 'tree_id'):
860 860 obj_data['tree'] = commit.tree_id.hex
861 861
862 862 return obj_data
863 863 return _revision(context_uid, repo_id, rev)
864 864
865 865 @reraise_safe_exceptions
866 866 def date(self, wire, commit_id):
867 867 cache_on, context_uid, repo_id = self._cache_on(wire)
868 868 region = self._region(wire)
869 869
870 870 @region.conditional_cache_on_arguments(condition=cache_on)
871 871 def _date(_repo_id, _commit_id):
872 872 repo_init = self._factory.repo_libgit2(wire)
873 873 with repo_init as repo:
874 874 commit = repo[commit_id]
875 875
876 876 if hasattr(commit, 'commit_time'):
877 877 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
878 878 else:
879 879 commit = commit.get_object()
880 880 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
881 881
882 882 # TODO(marcink): check dulwich difference of offset vs timezone
883 883 return [commit_time, commit_time_offset]
884 884 return _date(repo_id, commit_id)
885 885
886 886 @reraise_safe_exceptions
887 887 def author(self, wire, commit_id):
888 888 cache_on, context_uid, repo_id = self._cache_on(wire)
889 889 region = self._region(wire)
890 890
891 891 @region.conditional_cache_on_arguments(condition=cache_on)
892 892 def _author(_repo_id, _commit_id):
893 893 repo_init = self._factory.repo_libgit2(wire)
894 894 with repo_init as repo:
895 895 commit = repo[commit_id]
896 896
897 897 if hasattr(commit, 'author'):
898 898 author = commit.author
899 899 else:
900 900 author = commit.get_object().author
901 901
902 902 if author.email:
903 903 return "{} <{}>".format(author.name, author.email)
904 904
905 905 try:
906 906 return "{}".format(author.name)
907 907 except Exception:
908 908 return "{}".format(safe_str(author.raw_name))
909 909
910 910 return _author(repo_id, commit_id)
911 911
912 912 @reraise_safe_exceptions
913 913 def message(self, wire, commit_id):
914 914 cache_on, context_uid, repo_id = self._cache_on(wire)
915 915 region = self._region(wire)
916 916 @region.conditional_cache_on_arguments(condition=cache_on)
917 917 def _message(_repo_id, _commit_id):
918 918 repo_init = self._factory.repo_libgit2(wire)
919 919 with repo_init as repo:
920 920 commit = repo[commit_id]
921 921 return commit.message
922 922 return _message(repo_id, commit_id)
923 923
924 924 @reraise_safe_exceptions
925 925 def parents(self, wire, commit_id):
926 926 cache_on, context_uid, repo_id = self._cache_on(wire)
927 927 region = self._region(wire)
928 928
929 929 @region.conditional_cache_on_arguments(condition=cache_on)
930 930 def _parents(_repo_id, _commit_id):
931 931 repo_init = self._factory.repo_libgit2(wire)
932 932 with repo_init as repo:
933 933 commit = repo[commit_id]
934 934 if hasattr(commit, 'parent_ids'):
935 935 parent_ids = commit.parent_ids
936 936 else:
937 937 parent_ids = commit.get_object().parent_ids
938 938
939 939 return [x.hex for x in parent_ids]
940 940 return _parents(repo_id, commit_id)
941 941
942 942 @reraise_safe_exceptions
943 943 def children(self, wire, commit_id):
944 944 cache_on, context_uid, repo_id = self._cache_on(wire)
945 945 region = self._region(wire)
946 946
947 head = self.head(wire)
948
947 949 @region.conditional_cache_on_arguments(condition=cache_on)
948 950 def _children(_repo_id, _commit_id):
951
949 952 output, __ = self.run_git_command(
950 wire, ['rev-list', '--all', '--children'])
953 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
951 954
952 955 child_ids = []
953 pat = re.compile(r'^%s' % commit_id)
954 for l in output.splitlines():
955 if pat.match(l):
956 found_ids = l.split(' ')[1:]
956 pat = re.compile(r'^{}'.format(commit_id))
957 for line in output.splitlines():
958 line = safe_str(line)
959 if pat.match(line):
960 found_ids = line.split(' ')[1:]
957 961 child_ids.extend(found_ids)
962 break
958 963
959 964 return child_ids
960 965 return _children(repo_id, commit_id)
961 966
962 967 @reraise_safe_exceptions
963 968 def set_refs(self, wire, key, value):
964 969 repo_init = self._factory.repo_libgit2(wire)
965 970 with repo_init as repo:
966 971 repo.references.create(key, value, force=True)
967 972
968 973 @reraise_safe_exceptions
969 974 def create_branch(self, wire, branch_name, commit_id, force=False):
970 975 repo_init = self._factory.repo_libgit2(wire)
971 976 with repo_init as repo:
972 977 commit = repo[commit_id]
973 978
974 979 if force:
975 980 repo.branches.local.create(branch_name, commit, force=force)
976 981 elif not repo.branches.get(branch_name):
977 982 # create only if that branch isn't existing
978 983 repo.branches.local.create(branch_name, commit, force=force)
979 984
980 985 @reraise_safe_exceptions
981 986 def remove_ref(self, wire, key):
982 987 repo_init = self._factory.repo_libgit2(wire)
983 988 with repo_init as repo:
984 989 repo.references.delete(key)
985 990
986 991 @reraise_safe_exceptions
987 992 def tag_remove(self, wire, tag_name):
988 993 repo_init = self._factory.repo_libgit2(wire)
989 994 with repo_init as repo:
990 995 key = 'refs/tags/{}'.format(tag_name)
991 996 repo.references.delete(key)
992 997
993 998 @reraise_safe_exceptions
994 999 def tree_changes(self, wire, source_id, target_id):
995 1000 # TODO(marcink): remove this seems it's only used by tests
996 1001 repo = self._factory.repo(wire)
997 1002 source = repo[source_id].tree if source_id else None
998 1003 target = repo[target_id].tree
999 1004 result = repo.object_store.tree_changes(source, target)
1000 1005 return list(result)
1001 1006
1002 1007 @reraise_safe_exceptions
1003 1008 def tree_and_type_for_path(self, wire, commit_id, path):
1004 1009
1005 1010 cache_on, context_uid, repo_id = self._cache_on(wire)
1006 1011 region = self._region(wire)
1007 1012
1008 1013 @region.conditional_cache_on_arguments(condition=cache_on)
1009 1014 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1010 1015 repo_init = self._factory.repo_libgit2(wire)
1011 1016
1012 1017 with repo_init as repo:
1013 1018 commit = repo[commit_id]
1014 1019 try:
1015 1020 tree = commit.tree[path]
1016 1021 except KeyError:
1017 1022 return None, None, None
1018 1023
1019 1024 return tree.id.hex, tree.type_str, tree.filemode
1020 1025 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1021 1026
1022 1027 @reraise_safe_exceptions
1023 1028 def tree_items(self, wire, tree_id):
1024 1029 cache_on, context_uid, repo_id = self._cache_on(wire)
1025 1030 region = self._region(wire)
1026 1031
1027 1032 @region.conditional_cache_on_arguments(condition=cache_on)
1028 1033 def _tree_items(_repo_id, _tree_id):
1029 1034
1030 1035 repo_init = self._factory.repo_libgit2(wire)
1031 1036 with repo_init as repo:
1032 1037 try:
1033 1038 tree = repo[tree_id]
1034 1039 except KeyError:
1035 1040 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1036 1041
1037 1042 result = []
1038 1043 for item in tree:
1039 1044 item_sha = item.hex
1040 1045 item_mode = item.filemode
1041 1046 item_type = item.type_str
1042 1047
1043 1048 if item_type == 'commit':
1044 1049 # NOTE(marcink): submodules we translate to 'link' for backward compat
1045 1050 item_type = 'link'
1046 1051
1047 1052 result.append((item.name, item_mode, item_sha, item_type))
1048 1053 return result
1049 1054 return _tree_items(repo_id, tree_id)
1050 1055
1051 1056 @reraise_safe_exceptions
1052 1057 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1053 1058 """
1054 1059 Old version that uses subprocess to call diff
1055 1060 """
1056 1061
1057 1062 flags = [
1058 1063 '-U%s' % context, '--patch',
1059 1064 '--binary',
1060 1065 '--find-renames',
1061 1066 '--no-indent-heuristic',
1062 1067 # '--indent-heuristic',
1063 1068 #'--full-index',
1064 1069 #'--abbrev=40'
1065 1070 ]
1066 1071
1067 1072 if opt_ignorews:
1068 1073 flags.append('--ignore-all-space')
1069 1074
1070 1075 if commit_id_1 == self.EMPTY_COMMIT:
1071 1076 cmd = ['show'] + flags + [commit_id_2]
1072 1077 else:
1073 1078 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1074 1079
1075 1080 if file_filter:
1076 1081 cmd.extend(['--', file_filter])
1077 1082
1078 1083 diff, __ = self.run_git_command(wire, cmd)
1079 1084 # If we used 'show' command, strip first few lines (until actual diff
1080 1085 # starts)
1081 1086 if commit_id_1 == self.EMPTY_COMMIT:
1082 1087 lines = diff.splitlines()
1083 1088 x = 0
1084 1089 for line in lines:
1085 1090 if line.startswith(b'diff'):
1086 1091 break
1087 1092 x += 1
1088 1093 # Append new line just like 'diff' command do
1089 1094 diff = '\n'.join(lines[x:]) + '\n'
1090 1095 return diff
1091 1096
1092 1097 @reraise_safe_exceptions
1093 1098 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1094 1099 repo_init = self._factory.repo_libgit2(wire)
1095 1100 with repo_init as repo:
1096 1101 swap = True
1097 1102 flags = 0
1098 1103 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1099 1104
1100 1105 if opt_ignorews:
1101 1106 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1102 1107
1103 1108 if commit_id_1 == self.EMPTY_COMMIT:
1104 1109 comm1 = repo[commit_id_2]
1105 1110 diff_obj = comm1.tree.diff_to_tree(
1106 1111 flags=flags, context_lines=context, swap=swap)
1107 1112
1108 1113 else:
1109 1114 comm1 = repo[commit_id_2]
1110 1115 comm2 = repo[commit_id_1]
1111 1116 diff_obj = comm1.tree.diff_to_tree(
1112 1117 comm2.tree, flags=flags, context_lines=context, swap=swap)
1113 1118 similar_flags = 0
1114 1119 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1115 1120 diff_obj.find_similar(flags=similar_flags)
1116 1121
1117 1122 if file_filter:
1118 1123 for p in diff_obj:
1119 1124 if p.delta.old_file.path == file_filter:
1120 1125 return p.patch or ''
1121 1126 # fo matching path == no diff
1122 1127 return ''
1123 1128 return diff_obj.patch or ''
1124 1129
1125 1130 @reraise_safe_exceptions
1126 1131 def node_history(self, wire, commit_id, path, limit):
1127 1132 cache_on, context_uid, repo_id = self._cache_on(wire)
1128 1133 region = self._region(wire)
1129 1134
1130 1135 @region.conditional_cache_on_arguments(condition=cache_on)
1131 1136 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1132 1137 # optimize for n==1, rev-list is much faster for that use-case
1133 1138 if limit == 1:
1134 1139 cmd = ['rev-list', '-1', commit_id, '--', path]
1135 1140 else:
1136 1141 cmd = ['log']
1137 1142 if limit:
1138 1143 cmd.extend(['-n', str(safe_int(limit, 0))])
1139 1144 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1140 1145
1141 1146 output, __ = self.run_git_command(wire, cmd)
1142 1147 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1143 1148
1144 1149 return [x for x in commit_ids]
1145 1150 return _node_history(context_uid, repo_id, commit_id, path, limit)
1146 1151
1147 1152 @reraise_safe_exceptions
1148 1153 def node_annotate_legacy(self, wire, commit_id, path):
1149 1154 #note: replaced by pygit2 impelementation
1150 1155 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1151 1156 # -l ==> outputs long shas (and we need all 40 characters)
1152 1157 # --root ==> doesn't put '^' character for boundaries
1153 1158 # -r commit_id ==> blames for the given commit
1154 1159 output, __ = self.run_git_command(wire, cmd)
1155 1160
1156 1161 result = []
1157 1162 for i, blame_line in enumerate(output.splitlines()[:-1]):
1158 1163 line_no = i + 1
1159 1164 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1160 1165 result.append((line_no, blame_commit_id, line))
1161 1166
1162 1167 return result
1163 1168
1164 1169 @reraise_safe_exceptions
1165 1170 def node_annotate(self, wire, commit_id, path):
1166 1171
1167 1172 result_libgit = []
1168 1173 repo_init = self._factory.repo_libgit2(wire)
1169 1174 with repo_init as repo:
1170 1175 commit = repo[commit_id]
1171 1176 blame_obj = repo.blame(path, newest_commit=commit_id)
1172 1177 for i, line in enumerate(commit.tree[path].data.splitlines()):
1173 1178 line_no = i + 1
1174 1179 hunk = blame_obj.for_line(line_no)
1175 1180 blame_commit_id = hunk.final_commit_id.hex
1176 1181
1177 1182 result_libgit.append((line_no, blame_commit_id, line))
1178 1183
1179 1184 return result_libgit
1180 1185
1181 1186 @reraise_safe_exceptions
1182 1187 def update_server_info(self, wire):
1183 1188 repo = self._factory.repo(wire)
1184 1189 update_server_info(repo)
1185 1190
1186 1191 @reraise_safe_exceptions
1187 1192 def get_all_commit_ids(self, wire):
1188 1193
1189 1194 cache_on, context_uid, repo_id = self._cache_on(wire)
1190 1195 region = self._region(wire)
1191 1196
1192 1197 @region.conditional_cache_on_arguments(condition=cache_on)
1193 1198 def _get_all_commit_ids(_context_uid, _repo_id):
1194 1199
1195 1200 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1196 1201 try:
1197 1202 output, __ = self.run_git_command(wire, cmd)
1198 1203 return output.splitlines()
1199 1204 except Exception:
1200 1205 # Can be raised for empty repositories
1201 1206 return []
1202 1207
1203 1208 @region.conditional_cache_on_arguments(condition=cache_on)
1204 1209 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1205 1210 repo_init = self._factory.repo_libgit2(wire)
1206 1211 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1207 1212 results = []
1208 1213 with repo_init as repo:
1209 1214 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1210 1215 results.append(commit.id.hex)
1211 1216
1212 1217 return _get_all_commit_ids(context_uid, repo_id)
1213 1218
1214 1219 @reraise_safe_exceptions
1215 1220 def run_git_command(self, wire, cmd, **opts):
1216 1221 path = wire.get('path', None)
1217 1222
1218 1223 if path and os.path.isdir(path):
1219 1224 opts['cwd'] = path
1220 1225
1221 1226 if '_bare' in opts:
1222 1227 _copts = []
1223 1228 del opts['_bare']
1224 1229 else:
1225 1230 _copts = ['-c', 'core.quotepath=false', ]
1226 1231 safe_call = False
1227 1232 if '_safe' in opts:
1228 1233 # no exc on failure
1229 1234 del opts['_safe']
1230 1235 safe_call = True
1231 1236
1232 1237 if '_copts' in opts:
1233 1238 _copts.extend(opts['_copts'] or [])
1234 1239 del opts['_copts']
1235 1240
1236 1241 gitenv = os.environ.copy()
1237 1242 gitenv.update(opts.pop('extra_env', {}))
1238 1243 # need to clean fix GIT_DIR !
1239 1244 if 'GIT_DIR' in gitenv:
1240 1245 del gitenv['GIT_DIR']
1241 1246 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1242 1247 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1243 1248
1244 1249 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1245 1250 _opts = {'env': gitenv, 'shell': False}
1246 1251
1247 1252 proc = None
1248 1253 try:
1249 1254 _opts.update(opts)
1250 1255 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1251 1256
1252 1257 return b''.join(proc), b''.join(proc.stderr)
1253 1258 except OSError as err:
1254 1259 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1255 1260 tb_err = ("Couldn't run git command (%s).\n"
1256 1261 "Original error was:%s\n"
1257 1262 "Call options:%s\n"
1258 1263 % (cmd, err, _opts))
1259 1264 log.exception(tb_err)
1260 1265 if safe_call:
1261 1266 return '', err
1262 1267 else:
1263 1268 raise exceptions.VcsException()(tb_err)
1264 1269 finally:
1265 1270 if proc:
1266 1271 proc.close()
1267 1272
1268 1273 @reraise_safe_exceptions
1269 1274 def install_hooks(self, wire, force=False):
1270 1275 from vcsserver.hook_utils import install_git_hooks
1271 1276 bare = self.bare(wire)
1272 1277 path = wire['path']
1273 1278 return install_git_hooks(path, bare, force_create=force)
1274 1279
1275 1280 @reraise_safe_exceptions
1276 1281 def get_hooks_info(self, wire):
1277 1282 from vcsserver.hook_utils import (
1278 1283 get_git_pre_hook_version, get_git_post_hook_version)
1279 1284 bare = self.bare(wire)
1280 1285 path = wire['path']
1281 1286 return {
1282 1287 'pre_version': get_git_pre_hook_version(path, bare),
1283 1288 'post_version': get_git_post_hook_version(path, bare),
1284 1289 }
1285 1290
1286 1291 @reraise_safe_exceptions
1287 1292 def set_head_ref(self, wire, head_name):
1288 1293 log.debug('Setting refs/head to `%s`', head_name)
1289 1294 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1290 1295 output, __ = self.run_git_command(wire, cmd)
1291 1296 return [head_name] + output.splitlines()
1292 1297
1293 1298 @reraise_safe_exceptions
1294 1299 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1295 1300 archive_dir_name, commit_id):
1296 1301
1297 1302 def file_walker(_commit_id, path):
1298 1303 repo_init = self._factory.repo_libgit2(wire)
1299 1304
1300 1305 with repo_init as repo:
1301 1306 commit = repo[commit_id]
1302 1307
1303 1308 if path in ['', '/']:
1304 1309 tree = commit.tree
1305 1310 else:
1306 1311 tree = commit.tree[path.rstrip('/')]
1307 1312 tree_id = tree.id.hex
1308 1313 try:
1309 1314 tree = repo[tree_id]
1310 1315 except KeyError:
1311 1316 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1312 1317
1313 1318 index = LibGit2Index.Index()
1314 1319 index.read_tree(tree)
1315 1320 file_iter = index
1316 1321
1317 1322 for fn in file_iter:
1318 1323 file_path = fn.path
1319 1324 mode = fn.mode
1320 1325 is_link = stat.S_ISLNK(mode)
1321 1326 if mode == pygit2.GIT_FILEMODE_COMMIT:
1322 1327 log.debug('Skipping path %s as a commit node', file_path)
1323 1328 continue
1324 1329 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1325 1330
1326 1331 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1327 1332 archive_dir_name, commit_id)
General Comments 0
You need to be logged in to leave comments. Login now