##// END OF EJS Templates
feat(branch removal trough UI): Added delete_branch method for git. Fixes: RCCE-75
ilin.s -
r1246:f0acf3f6 default
parent child Browse files
Show More
@@ -1,1519 +1,1526 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 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 re
22 22 import stat
23 23 import traceback
24 24 import urllib.request
25 25 import urllib.parse
26 26 import urllib.error
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, FetchPackResult
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
41 41 import rhodecode
42 42 from vcsserver import exceptions, settings, subprocessio
43 43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_bytes, convert_to_str, splitnewlines
44 44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, store_archive_in_cache, BytesEnvelope, BinaryEnvelope
45 45 from vcsserver.hgcompat import (
46 46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 47 from vcsserver.git_lfs.lib import LFSOidStore
48 48 from vcsserver.vcs_base import RemoteBase
49 49
50 50 DIR_STAT = stat.S_IFDIR
51 51 FILE_MODE = stat.S_IFMT
52 52 GIT_LINK = objects.S_IFGITLINK
53 53 PEELED_REF_MARKER = b'^{}'
54 54 HEAD_MARKER = b'HEAD'
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def reraise_safe_exceptions(func):
60 60 """Converts Dulwich exceptions to something neutral."""
61 61
62 62 @wraps(func)
63 63 def wrapper(*args, **kwargs):
64 64 try:
65 65 return func(*args, **kwargs)
66 66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 67 exc = exceptions.LookupException(org_exc=e)
68 68 raise exc(safe_str(e))
69 69 except (HangupException, UnexpectedCommandError) as e:
70 70 exc = exceptions.VcsException(org_exc=e)
71 71 raise exc(safe_str(e))
72 72 except Exception:
73 73 # NOTE(marcink): because of how dulwich handles some exceptions
74 74 # (KeyError on empty repos), we cannot track this and catch all
75 75 # exceptions, it's an exceptions from other handlers
76 76 #if not hasattr(e, '_vcs_kind'):
77 77 #log.exception("Unhandled exception in git remote call")
78 78 #raise_from_original(exceptions.UnhandledException)
79 79 raise
80 80 return wrapper
81 81
82 82
83 83 class Repo(DulwichRepo):
84 84 """
85 85 A wrapper for dulwich Repo class.
86 86
87 87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 88 "Too many open files" error. We need to close all opened file descriptors
89 89 once the repo object is destroyed.
90 90 """
91 91 def __del__(self):
92 92 if hasattr(self, 'object_store'):
93 93 self.close()
94 94
95 95
96 96 class Repository(LibGit2Repo):
97 97
98 98 def __enter__(self):
99 99 return self
100 100
101 101 def __exit__(self, exc_type, exc_val, exc_tb):
102 102 self.free()
103 103
104 104
105 105 class GitFactory(RepoFactory):
106 106 repo_type = 'git'
107 107
108 108 def _create_repo(self, wire, create, use_libgit2=False):
109 109 if use_libgit2:
110 110 repo = Repository(safe_bytes(wire['path']))
111 111 else:
112 112 # dulwich mode
113 113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 114 repo = Repo(repo_path)
115 115
116 116 log.debug('repository created: got GIT object: %s', repo)
117 117 return repo
118 118
119 119 def repo(self, wire, create=False, use_libgit2=False):
120 120 """
121 121 Get a repository instance for the given path.
122 122 """
123 123 return self._create_repo(wire, create, use_libgit2)
124 124
125 125 def repo_libgit2(self, wire):
126 126 return self.repo(wire, use_libgit2=True)
127 127
128 128
129 129 def create_signature_from_string(author_str, **kwargs):
130 130 """
131 131 Creates a pygit2.Signature object from a string of the format 'Name <email>'.
132 132
133 133 :param author_str: String of the format 'Name <email>'
134 134 :return: pygit2.Signature object
135 135 """
136 136 match = re.match(r'^(.+) <(.+)>$', author_str)
137 137 if match is None:
138 138 raise ValueError(f"Invalid format: {author_str}")
139 139
140 140 name, email = match.groups()
141 141 return pygit2.Signature(name, email, **kwargs)
142 142
143 143
144 144 def get_obfuscated_url(url_obj):
145 145 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
146 146 url_obj.query = obfuscate_qs(url_obj.query)
147 147 obfuscated_uri = str(url_obj)
148 148 return obfuscated_uri
149 149
150 150
151 151 class GitRemote(RemoteBase):
152 152
153 153 def __init__(self, factory):
154 154 self._factory = factory
155 155 self._bulk_methods = {
156 156 "date": self.date,
157 157 "author": self.author,
158 158 "branch": self.branch,
159 159 "message": self.message,
160 160 "parents": self.parents,
161 161 "_commit": self.revision,
162 162 }
163 163 self._bulk_file_methods = {
164 164 "size": self.get_node_size,
165 165 "data": self.get_node_data,
166 166 "flags": self.get_node_flags,
167 167 "is_binary": self.get_node_is_binary,
168 168 "md5": self.md5_hash
169 169 }
170 170
171 171 def _wire_to_config(self, wire):
172 172 if 'config' in wire:
173 173 return {x[0] + '_' + x[1]: x[2] for x in wire['config']}
174 174 return {}
175 175
176 176 def _remote_conf(self, config):
177 177 params = [
178 178 '-c', 'core.askpass=""',
179 179 ]
180 180 config_attrs = {
181 181 'vcs_ssl_dir': 'http.sslCAinfo={}',
182 182 'vcs_git_lfs_store_location': 'lfs.storage={}'
183 183 }
184 184 for key, param in config_attrs.items():
185 185 if value := config.get(key):
186 186 params.extend(['-c', param.format(value)])
187 187 return params
188 188
189 189 @reraise_safe_exceptions
190 190 def discover_git_version(self):
191 191 stdout, _ = self.run_git_command(
192 192 {}, ['--version'], _bare=True, _safe=True)
193 193 prefix = b'git version'
194 194 if stdout.startswith(prefix):
195 195 stdout = stdout[len(prefix):]
196 196 return safe_str(stdout.strip())
197 197
198 198 @reraise_safe_exceptions
199 199 def is_empty(self, wire):
200 200 repo_init = self._factory.repo_libgit2(wire)
201 201 with repo_init as repo:
202 202 try:
203 203 has_head = repo.head.name
204 204 if has_head:
205 205 return False
206 206
207 207 # NOTE(marcink): check again using more expensive method
208 208 return repo.is_empty
209 209 except Exception:
210 210 pass
211 211
212 212 return True
213 213
214 214 @reraise_safe_exceptions
215 215 def assert_correct_path(self, wire):
216 216 cache_on, context_uid, repo_id = self._cache_on(wire)
217 217 region = self._region(wire)
218 218
219 219 @region.conditional_cache_on_arguments(condition=cache_on)
220 220 def _assert_correct_path(_context_uid, _repo_id, fast_check):
221 221 if fast_check:
222 222 path = safe_str(wire['path'])
223 223 if pygit2.discover_repository(path):
224 224 return True
225 225 return False
226 226 else:
227 227 try:
228 228 repo_init = self._factory.repo_libgit2(wire)
229 229 with repo_init:
230 230 pass
231 231 except pygit2.GitError:
232 232 path = wire.get('path')
233 233 tb = traceback.format_exc()
234 234 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
235 235 return False
236 236 return True
237 237
238 238 return _assert_correct_path(context_uid, repo_id, True)
239 239
240 240 @reraise_safe_exceptions
241 241 def bare(self, wire):
242 242 repo_init = self._factory.repo_libgit2(wire)
243 243 with repo_init as repo:
244 244 return repo.is_bare
245 245
246 246 @reraise_safe_exceptions
247 247 def get_node_data(self, wire, commit_id, path):
248 248 repo_init = self._factory.repo_libgit2(wire)
249 249 with repo_init as repo:
250 250 commit = repo[commit_id]
251 251 blob_obj = commit.tree[path]
252 252
253 253 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
254 254 raise exceptions.LookupException()(
255 255 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
256 256
257 257 return BytesEnvelope(blob_obj.data)
258 258
259 259 @reraise_safe_exceptions
260 260 def get_node_size(self, wire, commit_id, path):
261 261 repo_init = self._factory.repo_libgit2(wire)
262 262 with repo_init as repo:
263 263 commit = repo[commit_id]
264 264 blob_obj = commit.tree[path]
265 265
266 266 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
267 267 raise exceptions.LookupException()(
268 268 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
269 269
270 270 return blob_obj.size
271 271
272 272 @reraise_safe_exceptions
273 273 def get_node_flags(self, wire, commit_id, path):
274 274 repo_init = self._factory.repo_libgit2(wire)
275 275 with repo_init as repo:
276 276 commit = repo[commit_id]
277 277 blob_obj = commit.tree[path]
278 278
279 279 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
280 280 raise exceptions.LookupException()(
281 281 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
282 282
283 283 return blob_obj.filemode
284 284
285 285 @reraise_safe_exceptions
286 286 def get_node_is_binary(self, wire, commit_id, path):
287 287 repo_init = self._factory.repo_libgit2(wire)
288 288 with repo_init as repo:
289 289 commit = repo[commit_id]
290 290 blob_obj = commit.tree[path]
291 291
292 292 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
293 293 raise exceptions.LookupException()(
294 294 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
295 295
296 296 return blob_obj.is_binary
297 297
298 298 @reraise_safe_exceptions
299 299 def blob_as_pretty_string(self, wire, sha):
300 300 repo_init = self._factory.repo_libgit2(wire)
301 301 with repo_init as repo:
302 302 blob_obj = repo[sha]
303 303 return BytesEnvelope(blob_obj.data)
304 304
305 305 @reraise_safe_exceptions
306 306 def blob_raw_length(self, wire, sha):
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 _blob_raw_length(_repo_id, _sha):
312 312
313 313 repo_init = self._factory.repo_libgit2(wire)
314 314 with repo_init as repo:
315 315 blob = repo[sha]
316 316 return blob.size
317 317
318 318 return _blob_raw_length(repo_id, sha)
319 319
320 320 def _parse_lfs_pointer(self, raw_content):
321 321 spec_string = b'version https://git-lfs.github.com/spec'
322 322 if raw_content and raw_content.startswith(spec_string):
323 323
324 324 pattern = re.compile(rb"""
325 325 (?:\n)?
326 326 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
327 327 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
328 328 ^size[ ](?P<oid_size>[0-9]+)\n
329 329 (?:\n)?
330 330 """, re.VERBOSE | re.MULTILINE)
331 331 match = pattern.match(raw_content)
332 332 if match:
333 333 return match.groupdict()
334 334
335 335 return {}
336 336
337 337 @reraise_safe_exceptions
338 338 def is_large_file(self, wire, commit_id):
339 339 cache_on, context_uid, repo_id = self._cache_on(wire)
340 340 region = self._region(wire)
341 341
342 342 @region.conditional_cache_on_arguments(condition=cache_on)
343 343 def _is_large_file(_repo_id, _sha):
344 344 repo_init = self._factory.repo_libgit2(wire)
345 345 with repo_init as repo:
346 346 blob = repo[commit_id]
347 347 if blob.is_binary:
348 348 return {}
349 349
350 350 return self._parse_lfs_pointer(blob.data)
351 351
352 352 return _is_large_file(repo_id, commit_id)
353 353
354 354 @reraise_safe_exceptions
355 355 def is_binary(self, wire, tree_id):
356 356 cache_on, context_uid, repo_id = self._cache_on(wire)
357 357 region = self._region(wire)
358 358
359 359 @region.conditional_cache_on_arguments(condition=cache_on)
360 360 def _is_binary(_repo_id, _tree_id):
361 361 repo_init = self._factory.repo_libgit2(wire)
362 362 with repo_init as repo:
363 363 blob_obj = repo[tree_id]
364 364 return blob_obj.is_binary
365 365
366 366 return _is_binary(repo_id, tree_id)
367 367
368 368 @reraise_safe_exceptions
369 369 def md5_hash(self, wire, commit_id, path):
370 370 cache_on, context_uid, repo_id = self._cache_on(wire)
371 371 region = self._region(wire)
372 372
373 373 @region.conditional_cache_on_arguments(condition=cache_on)
374 374 def _md5_hash(_repo_id, _commit_id, _path):
375 375 repo_init = self._factory.repo_libgit2(wire)
376 376 with repo_init as repo:
377 377 commit = repo[_commit_id]
378 378 blob_obj = commit.tree[_path]
379 379
380 380 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
381 381 raise exceptions.LookupException()(
382 382 f'Tree for commit_id:{_commit_id} is not a blob: {blob_obj.type_str}')
383 383
384 384 return ''
385 385
386 386 return _md5_hash(repo_id, commit_id, path)
387 387
388 388 @reraise_safe_exceptions
389 389 def in_largefiles_store(self, wire, oid):
390 390 conf = self._wire_to_config(wire)
391 391 repo_init = self._factory.repo_libgit2(wire)
392 392 with repo_init as repo:
393 393 repo_name = repo.path
394 394
395 395 store_location = conf.get('vcs_git_lfs_store_location')
396 396 if store_location:
397 397
398 398 store = LFSOidStore(
399 399 oid=oid, repo=repo_name, store_location=store_location)
400 400 return store.has_oid()
401 401
402 402 return False
403 403
404 404 @reraise_safe_exceptions
405 405 def store_path(self, wire, oid):
406 406 conf = self._wire_to_config(wire)
407 407 repo_init = self._factory.repo_libgit2(wire)
408 408 with repo_init as repo:
409 409 repo_name = repo.path
410 410
411 411 store_location = conf.get('vcs_git_lfs_store_location')
412 412 if store_location:
413 413 store = LFSOidStore(
414 414 oid=oid, repo=repo_name, store_location=store_location)
415 415 return store.oid_path
416 416 raise ValueError(f'Unable to fetch oid with path {oid}')
417 417
418 418 @reraise_safe_exceptions
419 419 def bulk_request(self, wire, rev, pre_load):
420 420 cache_on, context_uid, repo_id = self._cache_on(wire)
421 421 region = self._region(wire)
422 422
423 423 @region.conditional_cache_on_arguments(condition=cache_on)
424 424 def _bulk_request(_repo_id, _rev, _pre_load):
425 425 result = {}
426 426 for attr in pre_load:
427 427 try:
428 428 method = self._bulk_methods[attr]
429 429 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
430 430 args = [wire, rev]
431 431 result[attr] = method(*args)
432 432 except KeyError as e:
433 433 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
434 434 return result
435 435
436 436 return _bulk_request(repo_id, rev, sorted(pre_load))
437 437
438 438 @reraise_safe_exceptions
439 439 def bulk_file_request(self, wire, commit_id, path, pre_load):
440 440 cache_on, context_uid, repo_id = self._cache_on(wire)
441 441 region = self._region(wire)
442 442
443 443 @region.conditional_cache_on_arguments(condition=cache_on)
444 444 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
445 445 result = {}
446 446 for attr in pre_load:
447 447 try:
448 448 method = self._bulk_file_methods[attr]
449 449 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
450 450 result[attr] = method(wire, _commit_id, _path)
451 451 except KeyError as e:
452 452 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
453 453 return result
454 454
455 455 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
456 456
457 457 def _build_opener(self, url: str):
458 458 handlers = []
459 459 url_obj = url_parser(safe_bytes(url))
460 460 authinfo = url_obj.authinfo()[1]
461 461
462 462 if authinfo:
463 463 # create a password manager
464 464 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
465 465 passmgr.add_password(*convert_to_str(authinfo))
466 466
467 467 handlers.extend((httpbasicauthhandler(passmgr),
468 468 httpdigestauthhandler(passmgr)))
469 469
470 470 return urllib.request.build_opener(*handlers)
471 471
472 472 @reraise_safe_exceptions
473 473 def check_url(self, url, config):
474 474 url_obj = url_parser(safe_bytes(url))
475 475
476 476 test_uri = safe_str(url_obj.authinfo()[0])
477 477 obfuscated_uri = get_obfuscated_url(url_obj)
478 478
479 479 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
480 480
481 481 if not test_uri.endswith('info/refs'):
482 482 test_uri = test_uri.rstrip('/') + '/info/refs'
483 483
484 484 o = self._build_opener(url=url)
485 485 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
486 486
487 487 q = {"service": 'git-upload-pack'}
488 488 qs = f'?{urllib.parse.urlencode(q)}'
489 489 cu = f"{test_uri}{qs}"
490 490
491 491 try:
492 492 req = urllib.request.Request(cu, None, {})
493 493 log.debug("Trying to open URL %s", obfuscated_uri)
494 494 resp = o.open(req)
495 495 if resp.code != 200:
496 496 raise exceptions.URLError()('Return Code is not 200')
497 497 except Exception as e:
498 498 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
499 499 # means it cannot be cloned
500 500 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
501 501
502 502 # now detect if it's proper git repo
503 503 gitdata: bytes = resp.read()
504 504
505 505 if b'service=git-upload-pack' in gitdata:
506 506 pass
507 507 elif re.findall(br'[0-9a-fA-F]{40}\s+refs', gitdata):
508 508 # old style git can return some other format!
509 509 pass
510 510 else:
511 511 e = None
512 512 raise exceptions.URLError(e)(
513 513 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
514 514
515 515 return True
516 516
517 517 @reraise_safe_exceptions
518 518 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
519 519 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
520 520 remote_refs = self.pull(wire, url, apply_refs=False)
521 521 repo = self._factory.repo(wire)
522 522 if isinstance(valid_refs, list):
523 523 valid_refs = tuple(valid_refs)
524 524
525 525 for k in remote_refs:
526 526 # only parse heads/tags and skip so called deferred tags
527 527 if k.startswith(valid_refs) and not k.endswith(deferred):
528 528 repo[k] = remote_refs[k]
529 529
530 530 if update_after_clone:
531 531 # we want to checkout HEAD
532 532 repo["HEAD"] = remote_refs["HEAD"]
533 533 index.build_index_from_tree(repo.path, repo.index_path(),
534 534 repo.object_store, repo["HEAD"].tree)
535 535
536 536 @reraise_safe_exceptions
537 537 def branch(self, wire, commit_id):
538 538 cache_on, context_uid, repo_id = self._cache_on(wire)
539 539 region = self._region(wire)
540 540
541 541 @region.conditional_cache_on_arguments(condition=cache_on)
542 542 def _branch(_context_uid, _repo_id, _commit_id):
543 543 regex = re.compile('^refs/heads')
544 544
545 545 def filter_with(ref):
546 546 return regex.match(ref[0]) and ref[1] == _commit_id
547 547
548 548 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
549 549 return [x[0].split('refs/heads/')[-1] for x in branches]
550 550
551 551 return _branch(context_uid, repo_id, commit_id)
552 552
553 553 @reraise_safe_exceptions
554 def delete_branch(self, wire, branch_name):
555 repo_init = self._factory.repo_libgit2(wire)
556 with repo_init as repo:
557 if branch := repo.lookup_branch(branch_name):
558 branch.delete()
559
560 @reraise_safe_exceptions
554 561 def commit_branches(self, wire, commit_id):
555 562 cache_on, context_uid, repo_id = self._cache_on(wire)
556 563 region = self._region(wire)
557 564
558 565 @region.conditional_cache_on_arguments(condition=cache_on)
559 566 def _commit_branches(_context_uid, _repo_id, _commit_id):
560 567 repo_init = self._factory.repo_libgit2(wire)
561 568 with repo_init as repo:
562 569 branches = [x for x in repo.branches.with_commit(_commit_id)]
563 570 return branches
564 571
565 572 return _commit_branches(context_uid, repo_id, commit_id)
566 573
567 574 @reraise_safe_exceptions
568 575 def add_object(self, wire, content):
569 576 repo_init = self._factory.repo_libgit2(wire)
570 577 with repo_init as repo:
571 578 blob = objects.Blob()
572 579 blob.set_raw_string(content)
573 580 repo.object_store.add_object(blob)
574 581 return blob.id
575 582
576 583 @reraise_safe_exceptions
577 584 def create_commit(self, wire, author, committer, message, branch, new_tree_id,
578 585 date_args: list[int, int] = None,
579 586 parents: list | None = None):
580 587
581 588 repo_init = self._factory.repo_libgit2(wire)
582 589 with repo_init as repo:
583 590
584 591 if date_args:
585 592 current_time, offset = date_args
586 593
587 594 kw = {
588 595 'time': current_time,
589 596 'offset': offset
590 597 }
591 598 author = create_signature_from_string(author, **kw)
592 599 committer = create_signature_from_string(committer, **kw)
593 600
594 601 tree = new_tree_id
595 602 if isinstance(tree, (bytes, str)):
596 603 # validate this tree is in the repo...
597 604 tree = repo[safe_str(tree)].id
598 605
599 606 if parents:
600 607 # run via sha's and validate them in repo
601 608 parents = [repo[c].id for c in parents]
602 609 else:
603 610 parents = []
604 611 # ensure we COMMIT on top of given branch head
605 612 # check if this repo has ANY branches, otherwise it's a new branch case we need to make
606 613 if branch in repo.branches.local:
607 614 parents += [repo.branches[branch].target]
608 615 elif [x for x in repo.branches.local]:
609 616 parents += [repo.head.target]
610 617 #else:
611 618 # in case we want to commit on new branch we create it on top of HEAD
612 619 #repo.branches.local.create(branch, repo.revparse_single('HEAD'))
613 620
614 621 # # Create a new commit
615 622 commit_oid = repo.create_commit(
616 623 f'refs/heads/{branch}', # the name of the reference to update
617 624 author, # the author of the commit
618 625 committer, # the committer of the commit
619 626 message, # the commit message
620 627 tree, # the tree produced by the index
621 628 parents # list of parents for the new commit, usually just one,
622 629 )
623 630
624 631 new_commit_id = safe_str(commit_oid)
625 632
626 633 return new_commit_id
627 634
628 635 @reraise_safe_exceptions
629 636 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
630 637
631 638 def mode2pygit(mode):
632 639 """
633 640 git only supports two filemode 644 and 755
634 641
635 642 0o100755 -> 33261
636 643 0o100644 -> 33188
637 644 """
638 645 return {
639 646 0o100644: pygit2.GIT_FILEMODE_BLOB,
640 647 0o100755: pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
641 648 0o120000: pygit2.GIT_FILEMODE_LINK
642 649 }.get(mode) or pygit2.GIT_FILEMODE_BLOB
643 650
644 651 repo_init = self._factory.repo_libgit2(wire)
645 652 with repo_init as repo:
646 653 repo_index = repo.index
647 654
648 655 commit_parents = None
649 656 if commit_tree and commit_data['parents']:
650 657 commit_parents = commit_data['parents']
651 658 parent_commit = repo[commit_parents[0]]
652 659 repo_index.read_tree(parent_commit.tree)
653 660
654 661 for pathspec in updated:
655 662 blob_id = repo.create_blob(pathspec['content'])
656 663 ie = pygit2.IndexEntry(pathspec['path'], blob_id, mode2pygit(pathspec['mode']))
657 664 repo_index.add(ie)
658 665
659 666 for pathspec in removed:
660 667 repo_index.remove(pathspec)
661 668
662 669 # Write changes to the index
663 670 repo_index.write()
664 671
665 672 # Create a tree from the updated index
666 673 written_commit_tree = repo_index.write_tree()
667 674
668 675 new_tree_id = written_commit_tree
669 676
670 677 author = commit_data['author']
671 678 committer = commit_data['committer']
672 679 message = commit_data['message']
673 680
674 681 date_args = [int(commit_data['commit_time']), int(commit_data['commit_timezone'])]
675 682
676 683 new_commit_id = self.create_commit(wire, author, committer, message, branch,
677 684 new_tree_id, date_args=date_args, parents=commit_parents)
678 685
679 686 # libgit2, ensure the branch is there and exists
680 687 self.create_branch(wire, branch, new_commit_id)
681 688
682 689 # libgit2, set new ref to this created commit
683 690 self.set_refs(wire, f'refs/heads/{branch}', new_commit_id)
684 691
685 692 return new_commit_id
686 693
687 694 @reraise_safe_exceptions
688 695 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
689 696 if url != 'default' and '://' not in url:
690 697 client = LocalGitClient(url)
691 698 else:
692 699 url_obj = url_parser(safe_bytes(url))
693 700 o = self._build_opener(url)
694 701 url = url_obj.authinfo()[0]
695 702 client = HttpGitClient(base_url=url, opener=o)
696 703 repo = self._factory.repo(wire)
697 704
698 705 determine_wants = repo.object_store.determine_wants_all
699 706
700 707 if refs:
701 708 refs: list[bytes] = [ascii_bytes(x) for x in refs]
702 709
703 710 def determine_wants_requested(_remote_refs):
704 711 determined = []
705 712 for ref_name, ref_hash in _remote_refs.items():
706 713 bytes_ref_name = safe_bytes(ref_name)
707 714
708 715 if bytes_ref_name in refs:
709 716 bytes_ref_hash = safe_bytes(ref_hash)
710 717 determined.append(bytes_ref_hash)
711 718 return determined
712 719
713 720 # swap with our custom requested wants
714 721 determine_wants = determine_wants_requested
715 722
716 723 try:
717 724 remote_refs = client.fetch(
718 725 path=url, target=repo, determine_wants=determine_wants)
719 726
720 727 except NotGitRepository as e:
721 728 log.warning(
722 729 'Trying to fetch from "%s" failed, not a Git repository.', url)
723 730 # Exception can contain unicode which we convert
724 731 raise exceptions.AbortException(e)(repr(e))
725 732
726 733 # mikhail: client.fetch() returns all the remote refs, but fetches only
727 734 # refs filtered by `determine_wants` function. We need to filter result
728 735 # as well
729 736 if refs:
730 737 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
731 738
732 739 if apply_refs:
733 740 # TODO: johbo: Needs proper test coverage with a git repository
734 741 # that contains a tag object, so that we would end up with
735 742 # a peeled ref at this point.
736 743 for k in remote_refs:
737 744 if k.endswith(PEELED_REF_MARKER):
738 745 log.debug("Skipping peeled reference %s", k)
739 746 continue
740 747 repo[k] = remote_refs[k]
741 748
742 749 if refs and not update_after:
743 750 # update to ref
744 751 # mikhail: explicitly set the head to the last ref.
745 752 update_to_ref = refs[-1]
746 753 if isinstance(update_after, str):
747 754 update_to_ref = update_after
748 755
749 756 repo[HEAD_MARKER] = remote_refs[update_to_ref]
750 757
751 758 if update_after:
752 759 # we want to check out HEAD
753 760 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
754 761 index.build_index_from_tree(repo.path, repo.index_path(),
755 762 repo.object_store, repo[HEAD_MARKER].tree)
756 763
757 764 if isinstance(remote_refs, FetchPackResult):
758 765 return remote_refs.refs
759 766 return remote_refs
760 767
761 768 @reraise_safe_exceptions
762 769 def sync_fetch(self, wire, url, refs=None, all_refs=False, **kwargs):
763 770 self._factory.repo(wire)
764 771 if refs and not isinstance(refs, (list, tuple)):
765 772 refs = [refs]
766 773
767 774 config = self._wire_to_config(wire)
768 775 # get all remote refs we'll use to fetch later
769 776 cmd = ['ls-remote']
770 777 if not all_refs:
771 778 cmd += ['--heads', '--tags']
772 779 cmd += [url]
773 780 output, __ = self.run_git_command(
774 781 wire, cmd, fail_on_stderr=False,
775 782 _copts=self._remote_conf(config),
776 783 extra_env={'GIT_TERMINAL_PROMPT': '0'})
777 784
778 785 remote_refs = collections.OrderedDict()
779 786 fetch_refs = []
780 787
781 788 for ref_line in output.splitlines():
782 789 sha, ref = ref_line.split(b'\t')
783 790 sha = sha.strip()
784 791 if ref in remote_refs:
785 792 # duplicate, skip
786 793 continue
787 794 if ref.endswith(PEELED_REF_MARKER):
788 795 log.debug("Skipping peeled reference %s", ref)
789 796 continue
790 797 # don't sync HEAD
791 798 if ref in [HEAD_MARKER]:
792 799 continue
793 800
794 801 remote_refs[ref] = sha
795 802
796 803 if refs and sha in refs:
797 804 # we filter fetch using our specified refs
798 805 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
799 806 elif not refs:
800 807 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
801 808 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
802 809
803 810 if fetch_refs:
804 811 for chunk in more_itertools.chunked(fetch_refs, 128):
805 812 fetch_refs_chunks = list(chunk)
806 813 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
807 814 self.run_git_command(
808 815 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
809 816 fail_on_stderr=False,
810 817 _copts=self._remote_conf(config),
811 818 extra_env={'GIT_TERMINAL_PROMPT': '0'})
812 819 if kwargs.get('sync_large_objects'):
813 820 self.run_git_command(
814 821 wire, ['lfs', 'fetch', url, '--all'],
815 822 fail_on_stderr=False,
816 823 _copts=self._remote_conf(config),
817 824 )
818 825
819 826 return remote_refs
820 827
821 828 @reraise_safe_exceptions
822 829 def sync_push(self, wire, url, refs=None, **kwargs):
823 830 if not self.check_url(url, wire):
824 831 return
825 832 config = self._wire_to_config(wire)
826 833 self._factory.repo(wire)
827 834 self.run_git_command(
828 835 wire, ['push', url, '--mirror'], fail_on_stderr=False,
829 836 _copts=self._remote_conf(config),
830 837 extra_env={'GIT_TERMINAL_PROMPT': '0'})
831 838 if kwargs.get('sync_large_objects'):
832 839 self.run_git_command(
833 840 wire, ['lfs', 'push', url, '--all'],
834 841 fail_on_stderr=False,
835 842 _copts=self._remote_conf(config),
836 843 )
837 844
838 845 @reraise_safe_exceptions
839 846 def get_remote_refs(self, wire, url):
840 847 repo = Repo(url)
841 848 return repo.get_refs()
842 849
843 850 @reraise_safe_exceptions
844 851 def get_description(self, wire):
845 852 repo = self._factory.repo(wire)
846 853 return repo.get_description()
847 854
848 855 @reraise_safe_exceptions
849 856 def get_missing_revs(self, wire, rev1, rev2, other_repo_path):
850 857 origin_repo_path = wire['path']
851 858 repo = self._factory.repo(wire)
852 859 # fetch from other_repo_path to our origin repo
853 860 LocalGitClient(thin_packs=False).fetch(other_repo_path, repo)
854 861
855 862 wire_remote = wire.copy()
856 863 wire_remote['path'] = other_repo_path
857 864 repo_remote = self._factory.repo(wire_remote)
858 865
859 866 # fetch from origin_repo_path to our remote repo
860 867 LocalGitClient(thin_packs=False).fetch(origin_repo_path, repo_remote)
861 868
862 869 revs = [
863 870 x.commit.id
864 871 for x in repo_remote.get_walker(include=[safe_bytes(rev2)], exclude=[safe_bytes(rev1)])]
865 872 return revs
866 873
867 874 @reraise_safe_exceptions
868 875 def get_object(self, wire, sha, maybe_unreachable=False):
869 876 cache_on, context_uid, repo_id = self._cache_on(wire)
870 877 region = self._region(wire)
871 878
872 879 @region.conditional_cache_on_arguments(condition=cache_on)
873 880 def _get_object(_context_uid, _repo_id, _sha):
874 881 repo_init = self._factory.repo_libgit2(wire)
875 882 with repo_init as repo:
876 883
877 884 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
878 885 try:
879 886 commit = repo.revparse_single(sha)
880 887 except KeyError:
881 888 # NOTE(marcink): KeyError doesn't give us any meaningful information
882 889 # here, we instead give something more explicit
883 890 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
884 891 raise exceptions.LookupException(e)(missing_commit_err)
885 892 except ValueError as e:
886 893 raise exceptions.LookupException(e)(missing_commit_err)
887 894
888 895 is_tag = False
889 896 if isinstance(commit, pygit2.Tag):
890 897 commit = repo.get(commit.target)
891 898 is_tag = True
892 899
893 900 check_dangling = True
894 901 if is_tag:
895 902 check_dangling = False
896 903
897 904 if check_dangling and maybe_unreachable:
898 905 check_dangling = False
899 906
900 907 # we used a reference and it parsed means we're not having a dangling commit
901 908 if sha != commit.hex:
902 909 check_dangling = False
903 910
904 911 if check_dangling:
905 912 # check for dangling commit
906 913 for branch in repo.branches.with_commit(commit.hex):
907 914 if branch:
908 915 break
909 916 else:
910 917 # NOTE(marcink): Empty error doesn't give us any meaningful information
911 918 # here, we instead give something more explicit
912 919 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
913 920 raise exceptions.LookupException(e)(missing_commit_err)
914 921
915 922 commit_id = commit.hex
916 923 type_str = commit.type_str
917 924
918 925 return {
919 926 'id': commit_id,
920 927 'type': type_str,
921 928 'commit_id': commit_id,
922 929 'idx': 0
923 930 }
924 931
925 932 return _get_object(context_uid, repo_id, sha)
926 933
927 934 @reraise_safe_exceptions
928 935 def get_refs(self, wire):
929 936 cache_on, context_uid, repo_id = self._cache_on(wire)
930 937 region = self._region(wire)
931 938
932 939 @region.conditional_cache_on_arguments(condition=cache_on)
933 940 def _get_refs(_context_uid, _repo_id):
934 941
935 942 repo_init = self._factory.repo_libgit2(wire)
936 943 with repo_init as repo:
937 944 regex = re.compile('^refs/(heads|tags)/')
938 945 return {x.name: x.target.hex for x in
939 946 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
940 947
941 948 return _get_refs(context_uid, repo_id)
942 949
943 950 @reraise_safe_exceptions
944 951 def get_branch_pointers(self, wire):
945 952 cache_on, context_uid, repo_id = self._cache_on(wire)
946 953 region = self._region(wire)
947 954
948 955 @region.conditional_cache_on_arguments(condition=cache_on)
949 956 def _get_branch_pointers(_context_uid, _repo_id):
950 957
951 958 repo_init = self._factory.repo_libgit2(wire)
952 959 regex = re.compile('^refs/heads')
953 960 with repo_init as repo:
954 961 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
955 962 return {x.target.hex: x.shorthand for x in branches}
956 963
957 964 return _get_branch_pointers(context_uid, repo_id)
958 965
959 966 @reraise_safe_exceptions
960 967 def head(self, wire, show_exc=True):
961 968 cache_on, context_uid, repo_id = self._cache_on(wire)
962 969 region = self._region(wire)
963 970
964 971 @region.conditional_cache_on_arguments(condition=cache_on)
965 972 def _head(_context_uid, _repo_id, _show_exc):
966 973 repo_init = self._factory.repo_libgit2(wire)
967 974 with repo_init as repo:
968 975 try:
969 976 return repo.head.peel().hex
970 977 except Exception:
971 978 if show_exc:
972 979 raise
973 980 return _head(context_uid, repo_id, show_exc)
974 981
975 982 @reraise_safe_exceptions
976 983 def init(self, wire):
977 984 repo_path = safe_str(wire['path'])
978 985 os.makedirs(repo_path, mode=0o755)
979 986 pygit2.init_repository(repo_path, bare=False)
980 987
981 988 @reraise_safe_exceptions
982 989 def init_bare(self, wire):
983 990 repo_path = safe_str(wire['path'])
984 991 os.makedirs(repo_path, mode=0o755)
985 992 pygit2.init_repository(repo_path, bare=True)
986 993
987 994 @reraise_safe_exceptions
988 995 def revision(self, wire, rev):
989 996
990 997 cache_on, context_uid, repo_id = self._cache_on(wire)
991 998 region = self._region(wire)
992 999
993 1000 @region.conditional_cache_on_arguments(condition=cache_on)
994 1001 def _revision(_context_uid, _repo_id, _rev):
995 1002 repo_init = self._factory.repo_libgit2(wire)
996 1003 with repo_init as repo:
997 1004 commit = repo[rev]
998 1005 obj_data = {
999 1006 'id': commit.id.hex,
1000 1007 }
1001 1008 # tree objects itself don't have tree_id attribute
1002 1009 if hasattr(commit, 'tree_id'):
1003 1010 obj_data['tree'] = commit.tree_id.hex
1004 1011
1005 1012 return obj_data
1006 1013 return _revision(context_uid, repo_id, rev)
1007 1014
1008 1015 @reraise_safe_exceptions
1009 1016 def date(self, wire, commit_id):
1010 1017 cache_on, context_uid, repo_id = self._cache_on(wire)
1011 1018 region = self._region(wire)
1012 1019
1013 1020 @region.conditional_cache_on_arguments(condition=cache_on)
1014 1021 def _date(_repo_id, _commit_id):
1015 1022 repo_init = self._factory.repo_libgit2(wire)
1016 1023 with repo_init as repo:
1017 1024 commit = repo[commit_id]
1018 1025
1019 1026 if hasattr(commit, 'commit_time'):
1020 1027 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1021 1028 else:
1022 1029 commit = commit.get_object()
1023 1030 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1024 1031
1025 1032 # TODO(marcink): check dulwich difference of offset vs timezone
1026 1033 return [commit_time, commit_time_offset]
1027 1034 return _date(repo_id, commit_id)
1028 1035
1029 1036 @reraise_safe_exceptions
1030 1037 def author(self, wire, commit_id):
1031 1038 cache_on, context_uid, repo_id = self._cache_on(wire)
1032 1039 region = self._region(wire)
1033 1040
1034 1041 @region.conditional_cache_on_arguments(condition=cache_on)
1035 1042 def _author(_repo_id, _commit_id):
1036 1043 repo_init = self._factory.repo_libgit2(wire)
1037 1044 with repo_init as repo:
1038 1045 commit = repo[commit_id]
1039 1046
1040 1047 if hasattr(commit, 'author'):
1041 1048 author = commit.author
1042 1049 else:
1043 1050 author = commit.get_object().author
1044 1051
1045 1052 if author.email:
1046 1053 return f"{author.name} <{author.email}>"
1047 1054
1048 1055 try:
1049 1056 return f"{author.name}"
1050 1057 except Exception:
1051 1058 return f"{safe_str(author.raw_name)}"
1052 1059
1053 1060 return _author(repo_id, commit_id)
1054 1061
1055 1062 @reraise_safe_exceptions
1056 1063 def message(self, wire, commit_id):
1057 1064 cache_on, context_uid, repo_id = self._cache_on(wire)
1058 1065 region = self._region(wire)
1059 1066
1060 1067 @region.conditional_cache_on_arguments(condition=cache_on)
1061 1068 def _message(_repo_id, _commit_id):
1062 1069 repo_init = self._factory.repo_libgit2(wire)
1063 1070 with repo_init as repo:
1064 1071 commit = repo[commit_id]
1065 1072 return commit.message
1066 1073 return _message(repo_id, commit_id)
1067 1074
1068 1075 @reraise_safe_exceptions
1069 1076 def parents(self, wire, commit_id):
1070 1077 cache_on, context_uid, repo_id = self._cache_on(wire)
1071 1078 region = self._region(wire)
1072 1079
1073 1080 @region.conditional_cache_on_arguments(condition=cache_on)
1074 1081 def _parents(_repo_id, _commit_id):
1075 1082 repo_init = self._factory.repo_libgit2(wire)
1076 1083 with repo_init as repo:
1077 1084 commit = repo[commit_id]
1078 1085 if hasattr(commit, 'parent_ids'):
1079 1086 parent_ids = commit.parent_ids
1080 1087 else:
1081 1088 parent_ids = commit.get_object().parent_ids
1082 1089
1083 1090 return [x.hex for x in parent_ids]
1084 1091 return _parents(repo_id, commit_id)
1085 1092
1086 1093 @reraise_safe_exceptions
1087 1094 def children(self, wire, commit_id):
1088 1095 cache_on, context_uid, repo_id = self._cache_on(wire)
1089 1096 region = self._region(wire)
1090 1097
1091 1098 head = self.head(wire)
1092 1099
1093 1100 @region.conditional_cache_on_arguments(condition=cache_on)
1094 1101 def _children(_repo_id, _commit_id):
1095 1102
1096 1103 output, __ = self.run_git_command(
1097 1104 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
1098 1105
1099 1106 child_ids = []
1100 1107 pat = re.compile(fr'^{commit_id}')
1101 1108 for line in output.splitlines():
1102 1109 line = safe_str(line)
1103 1110 if pat.match(line):
1104 1111 found_ids = line.split(' ')[1:]
1105 1112 child_ids.extend(found_ids)
1106 1113 break
1107 1114
1108 1115 return child_ids
1109 1116 return _children(repo_id, commit_id)
1110 1117
1111 1118 @reraise_safe_exceptions
1112 1119 def set_refs(self, wire, key, value):
1113 1120 repo_init = self._factory.repo_libgit2(wire)
1114 1121 with repo_init as repo:
1115 1122 repo.references.create(key, value, force=True)
1116 1123
1117 1124 @reraise_safe_exceptions
1118 1125 def update_refs(self, wire, key, value):
1119 1126 repo_init = self._factory.repo_libgit2(wire)
1120 1127 with repo_init as repo:
1121 1128 if key not in repo.references:
1122 1129 raise ValueError(f'Reference {key} not found in the repository')
1123 1130 repo.references.create(key, value, force=True)
1124 1131
1125 1132 @reraise_safe_exceptions
1126 1133 def create_branch(self, wire, branch_name, commit_id, force=False):
1127 1134 repo_init = self._factory.repo_libgit2(wire)
1128 1135 with repo_init as repo:
1129 1136 if commit_id:
1130 1137 commit = repo[commit_id]
1131 1138 else:
1132 1139 # if commit is not given just use the HEAD
1133 1140 commit = repo.head()
1134 1141
1135 1142 if force:
1136 1143 repo.branches.local.create(branch_name, commit, force=force)
1137 1144 elif not repo.branches.get(branch_name):
1138 1145 # create only if that branch isn't existing
1139 1146 repo.branches.local.create(branch_name, commit, force=force)
1140 1147
1141 1148 @reraise_safe_exceptions
1142 1149 def remove_ref(self, wire, key):
1143 1150 repo_init = self._factory.repo_libgit2(wire)
1144 1151 with repo_init as repo:
1145 1152 repo.references.delete(key)
1146 1153
1147 1154 @reraise_safe_exceptions
1148 1155 def tag_remove(self, wire, tag_name):
1149 1156 repo_init = self._factory.repo_libgit2(wire)
1150 1157 with repo_init as repo:
1151 1158 key = f'refs/tags/{tag_name}'
1152 1159 repo.references.delete(key)
1153 1160
1154 1161 @reraise_safe_exceptions
1155 1162 def tree_changes(self, wire, source_id, target_id):
1156 1163 repo = self._factory.repo(wire)
1157 1164 # source can be empty
1158 1165 source_id = safe_bytes(source_id if source_id else b'')
1159 1166 target_id = safe_bytes(target_id)
1160 1167
1161 1168 source = repo[source_id].tree if source_id else None
1162 1169 target = repo[target_id].tree
1163 1170 result = repo.object_store.tree_changes(source, target)
1164 1171
1165 1172 added = set()
1166 1173 modified = set()
1167 1174 deleted = set()
1168 1175 for (old_path, new_path), (_, _), (_, _) in list(result):
1169 1176 if new_path and old_path:
1170 1177 modified.add(new_path)
1171 1178 elif new_path and not old_path:
1172 1179 added.add(new_path)
1173 1180 elif not new_path and old_path:
1174 1181 deleted.add(old_path)
1175 1182
1176 1183 return list(added), list(modified), list(deleted)
1177 1184
1178 1185 @reraise_safe_exceptions
1179 1186 def tree_and_type_for_path(self, wire, commit_id, path):
1180 1187
1181 1188 cache_on, context_uid, repo_id = self._cache_on(wire)
1182 1189 region = self._region(wire)
1183 1190
1184 1191 @region.conditional_cache_on_arguments(condition=cache_on)
1185 1192 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1186 1193 repo_init = self._factory.repo_libgit2(wire)
1187 1194
1188 1195 with repo_init as repo:
1189 1196 commit = repo[commit_id]
1190 1197 try:
1191 1198 tree = commit.tree[path]
1192 1199 except KeyError:
1193 1200 return None, None, None
1194 1201
1195 1202 return tree.id.hex, tree.type_str, tree.filemode
1196 1203 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1197 1204
1198 1205 @reraise_safe_exceptions
1199 1206 def tree_items(self, wire, tree_id):
1200 1207 cache_on, context_uid, repo_id = self._cache_on(wire)
1201 1208 region = self._region(wire)
1202 1209
1203 1210 @region.conditional_cache_on_arguments(condition=cache_on)
1204 1211 def _tree_items(_repo_id, _tree_id):
1205 1212
1206 1213 repo_init = self._factory.repo_libgit2(wire)
1207 1214 with repo_init as repo:
1208 1215 try:
1209 1216 tree = repo[tree_id]
1210 1217 except KeyError:
1211 1218 raise ObjectMissing(f'No tree with id: {tree_id}')
1212 1219
1213 1220 result = []
1214 1221 for item in tree:
1215 1222 item_sha = item.hex
1216 1223 item_mode = item.filemode
1217 1224 item_type = item.type_str
1218 1225
1219 1226 if item_type == 'commit':
1220 1227 # NOTE(marcink): submodules we translate to 'link' for backward compat
1221 1228 item_type = 'link'
1222 1229
1223 1230 result.append((item.name, item_mode, item_sha, item_type))
1224 1231 return result
1225 1232 return _tree_items(repo_id, tree_id)
1226 1233
1227 1234 @reraise_safe_exceptions
1228 1235 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1229 1236 """
1230 1237 Old version that uses subprocess to call diff
1231 1238 """
1232 1239
1233 1240 flags = [
1234 1241 f'-U{context}', '--patch',
1235 1242 '--binary',
1236 1243 '--find-renames',
1237 1244 '--no-indent-heuristic',
1238 1245 # '--indent-heuristic',
1239 1246 #'--full-index',
1240 1247 #'--abbrev=40'
1241 1248 ]
1242 1249
1243 1250 if opt_ignorews:
1244 1251 flags.append('--ignore-all-space')
1245 1252
1246 1253 if commit_id_1 == self.EMPTY_COMMIT:
1247 1254 cmd = ['show'] + flags + [commit_id_2]
1248 1255 else:
1249 1256 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1250 1257
1251 1258 if file_filter:
1252 1259 cmd.extend(['--', file_filter])
1253 1260
1254 1261 diff, __ = self.run_git_command(wire, cmd)
1255 1262 # If we used 'show' command, strip first few lines (until actual diff
1256 1263 # starts)
1257 1264 if commit_id_1 == self.EMPTY_COMMIT:
1258 1265 lines = diff.splitlines()
1259 1266 x = 0
1260 1267 for line in lines:
1261 1268 if line.startswith(b'diff'):
1262 1269 break
1263 1270 x += 1
1264 1271 # Append new line just like 'diff' command do
1265 1272 diff = '\n'.join(lines[x:]) + '\n'
1266 1273 return diff
1267 1274
1268 1275 @reraise_safe_exceptions
1269 1276 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1270 1277 repo_init = self._factory.repo_libgit2(wire)
1271 1278
1272 1279 with repo_init as repo:
1273 1280 swap = True
1274 1281 flags = 0
1275 1282 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1276 1283
1277 1284 if opt_ignorews:
1278 1285 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1279 1286
1280 1287 if commit_id_1 == self.EMPTY_COMMIT:
1281 1288 comm1 = repo[commit_id_2]
1282 1289 diff_obj = comm1.tree.diff_to_tree(
1283 1290 flags=flags, context_lines=context, swap=swap)
1284 1291
1285 1292 else:
1286 1293 comm1 = repo[commit_id_2]
1287 1294 comm2 = repo[commit_id_1]
1288 1295 diff_obj = comm1.tree.diff_to_tree(
1289 1296 comm2.tree, flags=flags, context_lines=context, swap=swap)
1290 1297 similar_flags = 0
1291 1298 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1292 1299 diff_obj.find_similar(flags=similar_flags)
1293 1300
1294 1301 if file_filter:
1295 1302 for p in diff_obj:
1296 1303 if p.delta.old_file.path == file_filter:
1297 1304 return BytesEnvelope(p.data) or BytesEnvelope(b'')
1298 1305 # fo matching path == no diff
1299 1306 return BytesEnvelope(b'')
1300 1307
1301 1308 return BytesEnvelope(safe_bytes(diff_obj.patch)) or BytesEnvelope(b'')
1302 1309
1303 1310 @reraise_safe_exceptions
1304 1311 def node_history(self, wire, commit_id, path, limit):
1305 1312 cache_on, context_uid, repo_id = self._cache_on(wire)
1306 1313 region = self._region(wire)
1307 1314
1308 1315 @region.conditional_cache_on_arguments(condition=cache_on)
1309 1316 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1310 1317 # optimize for n==1, rev-list is much faster for that use-case
1311 1318 if limit == 1:
1312 1319 cmd = ['rev-list', '-1', commit_id, '--', path]
1313 1320 else:
1314 1321 cmd = ['log']
1315 1322 if limit:
1316 1323 cmd.extend(['-n', str(safe_int(limit, 0))])
1317 1324 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1318 1325
1319 1326 output, __ = self.run_git_command(wire, cmd)
1320 1327 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1321 1328
1322 1329 return [x for x in commit_ids]
1323 1330 return _node_history(context_uid, repo_id, commit_id, path, limit)
1324 1331
1325 1332 @reraise_safe_exceptions
1326 1333 def node_annotate_legacy(self, wire, commit_id, path):
1327 1334 # note: replaced by pygit2 implementation
1328 1335 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1329 1336 # -l ==> outputs long shas (and we need all 40 characters)
1330 1337 # --root ==> doesn't put '^' character for boundaries
1331 1338 # -r commit_id ==> blames for the given commit
1332 1339 output, __ = self.run_git_command(wire, cmd)
1333 1340
1334 1341 result = []
1335 1342 for i, blame_line in enumerate(output.splitlines()[:-1]):
1336 1343 line_no = i + 1
1337 1344 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1338 1345 result.append((line_no, blame_commit_id, line))
1339 1346
1340 1347 return result
1341 1348
1342 1349 @reraise_safe_exceptions
1343 1350 def node_annotate(self, wire, commit_id, path):
1344 1351
1345 1352 result_libgit = []
1346 1353 repo_init = self._factory.repo_libgit2(wire)
1347 1354 with repo_init as repo:
1348 1355 commit = repo[commit_id]
1349 1356 blame_obj = repo.blame(path, newest_commit=commit_id)
1350 1357 file_content = commit.tree[path].data
1351 1358 for i, line in enumerate(splitnewlines(file_content)):
1352 1359 line_no = i + 1
1353 1360 hunk = blame_obj.for_line(line_no)
1354 1361 blame_commit_id = hunk.final_commit_id.hex
1355 1362
1356 1363 result_libgit.append((line_no, blame_commit_id, line))
1357 1364
1358 1365 return BinaryEnvelope(result_libgit)
1359 1366
1360 1367 @reraise_safe_exceptions
1361 1368 def update_server_info(self, wire, force=False):
1362 1369 cmd = ['update-server-info']
1363 1370 if force:
1364 1371 cmd += ['--force']
1365 1372 output, __ = self.run_git_command(wire, cmd)
1366 1373 return output.splitlines()
1367 1374
1368 1375 @reraise_safe_exceptions
1369 1376 def get_all_commit_ids(self, wire):
1370 1377
1371 1378 cache_on, context_uid, repo_id = self._cache_on(wire)
1372 1379 region = self._region(wire)
1373 1380
1374 1381 @region.conditional_cache_on_arguments(condition=cache_on)
1375 1382 def _get_all_commit_ids(_context_uid, _repo_id):
1376 1383
1377 1384 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1378 1385 try:
1379 1386 output, __ = self.run_git_command(wire, cmd)
1380 1387 return output.splitlines()
1381 1388 except Exception:
1382 1389 # Can be raised for empty repositories
1383 1390 return []
1384 1391
1385 1392 @region.conditional_cache_on_arguments(condition=cache_on)
1386 1393 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1387 1394 repo_init = self._factory.repo_libgit2(wire)
1388 1395 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1389 1396 results = []
1390 1397 with repo_init as repo:
1391 1398 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1392 1399 results.append(commit.id.hex)
1393 1400
1394 1401 return _get_all_commit_ids(context_uid, repo_id)
1395 1402
1396 1403 @reraise_safe_exceptions
1397 1404 def run_git_command(self, wire, cmd, **opts):
1398 1405 path = wire.get('path', None)
1399 1406 debug_mode = rhodecode.ConfigGet().get_bool('debug')
1400 1407
1401 1408 if path and os.path.isdir(path):
1402 1409 opts['cwd'] = path
1403 1410
1404 1411 if '_bare' in opts:
1405 1412 _copts = []
1406 1413 del opts['_bare']
1407 1414 else:
1408 1415 _copts = ['-c', 'core.quotepath=false', '-c', 'advice.diverging=false']
1409 1416 safe_call = False
1410 1417 if '_safe' in opts:
1411 1418 # no exc on failure
1412 1419 del opts['_safe']
1413 1420 safe_call = True
1414 1421
1415 1422 if '_copts' in opts:
1416 1423 _copts.extend(opts['_copts'] or [])
1417 1424 del opts['_copts']
1418 1425
1419 1426 gitenv = os.environ.copy()
1420 1427 gitenv.update(opts.pop('extra_env', {}))
1421 1428 # need to clean fix GIT_DIR !
1422 1429 if 'GIT_DIR' in gitenv:
1423 1430 del gitenv['GIT_DIR']
1424 1431 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1425 1432 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1426 1433
1427 1434 cmd = [settings.GIT_EXECUTABLE()] + _copts + cmd
1428 1435 _opts = {'env': gitenv, 'shell': False}
1429 1436
1430 1437 proc = None
1431 1438 try:
1432 1439 _opts.update(opts)
1433 1440 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1434 1441
1435 1442 return b''.join(proc), b''.join(proc.stderr)
1436 1443 except OSError as err:
1437 1444 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1438 1445 call_opts = {}
1439 1446 if debug_mode:
1440 1447 call_opts = _opts
1441 1448
1442 1449 tb_err = ("Couldn't run git command ({}).\n"
1443 1450 "Original error was:{}\n"
1444 1451 "Call options:{}\n"
1445 1452 .format(cmd, err, call_opts))
1446 1453 log.exception(tb_err)
1447 1454 if safe_call:
1448 1455 return '', err
1449 1456 else:
1450 1457 raise exceptions.VcsException()(tb_err)
1451 1458 finally:
1452 1459 if proc:
1453 1460 proc.close()
1454 1461
1455 1462 @reraise_safe_exceptions
1456 1463 def install_hooks(self, wire, force=False):
1457 1464 from vcsserver.hook_utils import install_git_hooks
1458 1465 bare = self.bare(wire)
1459 1466 path = wire['path']
1460 1467 binary_dir = settings.BINARY_DIR
1461 1468 if binary_dir:
1462 1469 os.path.join(binary_dir, 'python3')
1463 1470 return install_git_hooks(path, bare, force_create=force)
1464 1471
1465 1472 @reraise_safe_exceptions
1466 1473 def get_hooks_info(self, wire):
1467 1474 from vcsserver.hook_utils import (
1468 1475 get_git_pre_hook_version, get_git_post_hook_version)
1469 1476 bare = self.bare(wire)
1470 1477 path = wire['path']
1471 1478 return {
1472 1479 'pre_version': get_git_pre_hook_version(path, bare),
1473 1480 'post_version': get_git_post_hook_version(path, bare),
1474 1481 }
1475 1482
1476 1483 @reraise_safe_exceptions
1477 1484 def set_head_ref(self, wire, head_name):
1478 1485 log.debug('Setting refs/head to `%s`', head_name)
1479 1486 repo_init = self._factory.repo_libgit2(wire)
1480 1487 with repo_init as repo:
1481 1488 repo.set_head(f'refs/heads/{head_name}')
1482 1489
1483 1490 return [head_name] + [f'set HEAD to refs/heads/{head_name}']
1484 1491
1485 1492 @reraise_safe_exceptions
1486 1493 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1487 1494 archive_dir_name, commit_id, cache_config):
1488 1495
1489 1496 def file_walker(_commit_id, path):
1490 1497 repo_init = self._factory.repo_libgit2(wire)
1491 1498
1492 1499 with repo_init as repo:
1493 1500 commit = repo[commit_id]
1494 1501
1495 1502 if path in ['', '/']:
1496 1503 tree = commit.tree
1497 1504 else:
1498 1505 tree = commit.tree[path.rstrip('/')]
1499 1506 tree_id = tree.id.hex
1500 1507 try:
1501 1508 tree = repo[tree_id]
1502 1509 except KeyError:
1503 1510 raise ObjectMissing(f'No tree with id: {tree_id}')
1504 1511
1505 1512 index = LibGit2Index.Index()
1506 1513 index.read_tree(tree)
1507 1514 file_iter = index
1508 1515
1509 1516 for file_node in file_iter:
1510 1517 file_path = file_node.path
1511 1518 mode = file_node.mode
1512 1519 is_link = stat.S_ISLNK(mode)
1513 1520 if mode == pygit2.GIT_FILEMODE_COMMIT:
1514 1521 log.debug('Skipping path %s as a commit node', file_path)
1515 1522 continue
1516 1523 yield ArchiveNode(file_path, mode, is_link, repo[file_node.hex].read_raw)
1517 1524
1518 1525 return store_archive_in_cache(
1519 1526 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
General Comments 0
You need to be logged in to leave comments. Login now