github.com/whamcloud/lemur@v0.0.0-20190827193804-4655df8a52af/packaging/ci/lambda/GitPullS3/pygit2/remote.py (about) 1 # -*- coding: utf-8 -*- 2 # 3 # Copyright 2010-2015 The pygit2 contributors 4 # 5 # This file is free software; you can redistribute it and/or modify 6 # it under the terms of the GNU General Public License, version 2, 7 # as published by the Free Software Foundation. 8 # 9 # In addition to the permissions in the GNU General Public License, 10 # the authors give you unlimited permission to link the compiled 11 # version of this file into combinations with other programs, 12 # and to distribute those combinations without any restriction 13 # coming from the use of this file. (The General Public License 14 # restrictions do apply in other respects; for example, they cover 15 # modification of the file, and distribution when not linked into 16 # a combined executable.) 17 # 18 # This file is distributed in the hope that it will be useful, but 19 # WITHOUT ANY WARRANTY; without even the implied warranty of 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21 # General Public License for more details. 22 # 23 # You should have received a copy of the GNU General Public License 24 # along with this program; see the file COPYING. If not, write to 25 # the Free Software Foundation, 51 Franklin Street, Fifth Floor, 26 # Boston, MA 02110-1301, USA. 27 28 # Import from the future 29 from __future__ import absolute_import 30 31 # Import from pygit2 32 from _pygit2 import Oid 33 from .errors import check_error, Passthrough 34 from .ffi import ffi, C 35 from .refspec import Refspec 36 from .utils import to_bytes, strarray_to_strings, StrArray 37 38 39 def maybe_string(ptr): 40 if not ptr: 41 return None 42 43 return ffi.string(ptr).decode() 44 45 46 class TransferProgress(object): 47 """Progress downloading and indexing data during a fetch""" 48 49 def __init__(self, tp): 50 51 self.total_objects = tp.total_objects 52 """Total number of objects to download""" 53 54 self.indexed_objects = tp.indexed_objects 55 """Objects which have been indexed""" 56 57 self.received_objects = tp.received_objects 58 """Objects which have been received up to now""" 59 60 self.local_objects = tp.local_objects 61 """Local objects which were used to fix the thin pack""" 62 63 self.total_deltas = tp.total_deltas 64 """Total number of deltas in the pack""" 65 66 self.indexed_deltas = tp.indexed_deltas 67 """Deltas which have been indexed""" 68 69 self.received_bytes = tp.received_bytes 70 """"Number of bytes received up to now""" 71 72 73 class RemoteCallbacks(object): 74 """Base class for pygit2 remote callbacks. 75 76 Inherit from this class and override the callbacks which you want to use 77 in your class, which you can then pass to the network operations. 78 """ 79 80 def __init__(self, credentials=None, certificate=None): 81 """Initialize some callbacks in-line 82 83 Use this constructor to provide credentials and certificate 84 callbacks in-line, instead of defining your own class for these ones. 85 86 You can e.g. also pass in one of the credential objects as 'credentials' 87 instead of creating a function which returns a hard-coded object. 88 """ 89 90 if credentials is not None: 91 self.credentials = credentials 92 if certificate is not None: 93 self.certificate = certificate 94 95 def sideband_progress(self, string): 96 """Progress output callback 97 98 Override this function with your own progress reporting function 99 100 :param str string: Progress output from the remote 101 """ 102 103 def credentials(self, url, username_from_url, allowed_types): 104 """Credentials callback 105 106 If the remote server requires authentication, this function will 107 be called and its return value used for authentication. Override 108 it if you want to be able to perform authentication. 109 110 Parameters: 111 112 - url (str) -- The url of the remote. 113 114 - username_from_url (str or None) -- Username extracted from the url, 115 if any. 116 117 - allowed_types (int) -- Credential types supported by the remote. 118 119 Return value: credential 120 """ 121 raise Passthrough 122 123 def certificate_check(self, certificate, valid, host): 124 """Certificate callback 125 126 Override with your own function to determine whether the accept 127 the server's certificate. 128 129 :param None certificate: The certificate. It is currently always None 130 while we figure out how to represent it cross-platform 131 132 :param bool valid: Whether the TLS/SSH library thinks the certificate 133 is valid 134 135 :param str host: The hostname we want to connect to 136 137 Return value: True to connect, False to abort 138 """ 139 140 raise Passthrough 141 142 def transfer_progress(self, stats): 143 """Transfer progress callback 144 145 Override with your own function to report transfer progress. 146 147 :param TransferProgress stats: The progress up to now 148 """ 149 150 def update_tips(self, refname, old, new): 151 """Update tips callabck 152 153 Override with your own function to report reference updates 154 155 :param str refname: the name of the reference that's being updated 156 :param Oid old: the reference's old value 157 :param Oid new: the reference's new value 158 """ 159 160 def push_update_reference(self, refname, message): 161 """Push update reference callback 162 163 Override with your own function to report the remote's 164 acceptace or rejection of reference updates. 165 166 :param str refname: the name of the reference (on the remote) 167 :param str messsage: rejection message from the remote. If None, the update was accepted. 168 """ 169 170 def _fill_fetch_options(self, fetch_opts): 171 fetch_opts.callbacks.sideband_progress = self._sideband_progress_cb 172 fetch_opts.callbacks.transfer_progress = self._transfer_progress_cb 173 fetch_opts.callbacks.update_tips = self._update_tips_cb 174 fetch_opts.callbacks.credentials = self._credentials_cb 175 fetch_opts.callbacks.certificate_check = self._certificate_cb 176 # We need to make sure that this handle stays alive 177 self._self_handle = ffi.new_handle(self) 178 fetch_opts.callbacks.payload = self._self_handle 179 180 self._stored_exception = None 181 182 def _fill_push_options(self, push_opts): 183 push_opts.callbacks.sideband_progress = self._sideband_progress_cb 184 push_opts.callbacks.transfer_progress = self._transfer_progress_cb 185 push_opts.callbacks.update_tips = self._update_tips_cb 186 push_opts.callbacks.credentials = self._credentials_cb 187 push_opts.callbacks.certificate_check = self._certificate_cb 188 push_opts.callbacks.push_update_reference = self._push_update_reference_cb 189 # We need to make sure that this handle stays alive 190 self._self_handle = ffi.new_handle(self) 191 push_opts.callbacks.payload = self._self_handle 192 193 # These functions exist to be called by the git_remote as 194 # callbacks. They proxy the call to whatever the user set 195 196 @ffi.callback('git_transfer_progress_cb') 197 def _transfer_progress_cb(stats_ptr, data): 198 self = ffi.from_handle(data) 199 200 transfer_progress = getattr(self, 'transfer_progress', None) 201 if not transfer_progress: 202 return 0 203 204 try: 205 transfer_progress(TransferProgress(stats_ptr)) 206 except Exception as e: 207 self._stored_exception = e 208 return C.GIT_EUSER 209 210 return 0 211 212 @ffi.callback('git_transport_message_cb') 213 def _sideband_progress_cb(string, length, data): 214 self = ffi.from_handle(data) 215 216 progress = getattr(self, 'progress', None) 217 if not progress: 218 return 0 219 220 try: 221 s = ffi.string(string, length).decode() 222 progress(s) 223 except Exception as e: 224 self._stored_exception = e 225 return C.GIT_EUSER 226 227 return 0 228 229 @ffi.callback('int (*update_tips)(const char *refname, const git_oid *a,' 230 'const git_oid *b, void *data)') 231 def _update_tips_cb(refname, a, b, data): 232 self = ffi.from_handle(data) 233 234 update_tips = getattr(self, 'update_tips', None) 235 if not update_tips: 236 return 0 237 238 try: 239 s = maybe_string(refname) 240 a = Oid(raw=bytes(ffi.buffer(a)[:])) 241 b = Oid(raw=bytes(ffi.buffer(b)[:])) 242 243 update_tips(s, a, b) 244 except Exception as e: 245 self._stored_exception = e 246 return C.GIT_EUSER 247 248 return 0 249 250 @ffi.callback("int (*push_update_reference)(const char *ref, const char *msg, void *data)") 251 def _push_update_reference_cb(ref, msg, data): 252 self = ffi.from_handle(data) 253 254 push_update_reference = getattr(self, 'push_update_reference', None) 255 if not push_update_reference: 256 return 0 257 258 try: 259 refname = ffi.string(ref) 260 message = maybe_string(msg) 261 push_update_reference(refname, message) 262 except Exception as e: 263 self._stored_exception = e 264 return C.GIT_EUSER 265 266 return 0 267 268 @ffi.callback('int (*credentials)(git_cred **cred, const char *url,' 269 'const char *username_from_url, unsigned int allowed_types,' 270 'void *data)') 271 def _credentials_cb(cred_out, url, username, allowed, data): 272 self = ffi.from_handle(data) 273 274 credentials = getattr(self, 'credentials', None) 275 if not credentials: 276 return 0 277 278 try: 279 ccred = get_credentials(credentials, url, username, allowed) 280 cred_out[0] = ccred[0] 281 282 except Exception as e: 283 if e is Passthrough: 284 return C.GIT_PASSTHROUGH 285 286 self._stored_exception = e 287 return C.GIT_EUSER 288 289 return 0 290 291 @ffi.callback('int (*git_transport_certificate_check_cb)' 292 '(git_cert *cert, int valid, const char *host, void *payload)') 293 def _certificate_cb(cert_i, valid, host, data): 294 self = ffi.from_handle(data) 295 296 # We want to simulate what should happen if libgit2 supported pass-through for 297 # this callback. For SSH, 'valid' is always False, because it doesn't look 298 # at known_hosts, but we do want to let it through in order to do what libgit2 would 299 # if the callback were not set. 300 try: 301 is_ssh = cert_i.cert_type == C.GIT_CERT_HOSTKEY_LIBSSH2 302 303 certificate_check = getattr(self, 'certificate_check', None) 304 if not certificate_check: 305 raise Passthrough 306 307 # python's parsing is deep in the libraries and assumes an OpenSSL-owned cert 308 val = certificate_check(None, bool(valid), ffi.string(host)) 309 if not val: 310 return C.GIT_ECERTIFICATE 311 except Exception as e: 312 if e is Passthrough: 313 if is_ssh: 314 return 0 315 elif valid: 316 return 0 317 else: 318 return C.GIT_ECERTIFICATE 319 320 self._stored_exception = e 321 return C.GIT_EUSER 322 323 return 0 324 325 class Remote(object): 326 def __init__(self, repo, ptr): 327 """The constructor is for internal use only""" 328 329 self._repo = repo 330 self._remote = ptr 331 self._stored_exception = None 332 333 def __del__(self): 334 C.git_remote_free(self._remote) 335 336 @property 337 def name(self): 338 """Name of the remote""" 339 340 return maybe_string(C.git_remote_name(self._remote)) 341 342 @property 343 def url(self): 344 """Url of the remote""" 345 346 return maybe_string(C.git_remote_url(self._remote)) 347 348 @property 349 def push_url(self): 350 """Push url of the remote""" 351 352 return maybe_string(C.git_remote_pushurl(self._remote)) 353 354 def save(self): 355 """Save a remote to its repository's configuration.""" 356 357 err = C.git_remote_save(self._remote) 358 check_error(err) 359 360 def fetch(self, refspecs=None, message=None, callbacks=None): 361 """Perform a fetch against this remote. Returns a <TransferProgress> 362 object. 363 """ 364 365 fetch_opts = ffi.new('git_fetch_options *') 366 err = C.git_fetch_init_options(fetch_opts, C.GIT_FETCH_OPTIONS_VERSION) 367 368 if callbacks is None: 369 callbacks = RemoteCallbacks() 370 371 callbacks._fill_fetch_options(fetch_opts) 372 373 try: 374 with StrArray(refspecs) as arr: 375 err = C.git_remote_fetch(self._remote, arr, fetch_opts, to_bytes(message)) 376 if callbacks._stored_exception: 377 raise callbacks._stored_exception 378 check_error(err) 379 finally: 380 callbacks._self_handle = None 381 382 return TransferProgress(C.git_remote_stats(self._remote)) 383 384 @property 385 def refspec_count(self): 386 """Total number of refspecs in this remote""" 387 388 return C.git_remote_refspec_count(self._remote) 389 390 def get_refspec(self, n): 391 """Return the <Refspec> object at the given position.""" 392 spec = C.git_remote_get_refspec(self._remote, n) 393 return Refspec(self, spec) 394 395 @property 396 def fetch_refspecs(self): 397 """Refspecs that will be used for fetching""" 398 399 specs = ffi.new('git_strarray *') 400 err = C.git_remote_get_fetch_refspecs(specs, self._remote) 401 check_error(err) 402 403 return strarray_to_strings(specs) 404 405 @property 406 def push_refspecs(self): 407 """Refspecs that will be used for pushing""" 408 409 specs = ffi.new('git_strarray *') 410 err = C.git_remote_get_push_refspecs(specs, self._remote) 411 check_error(err) 412 413 return strarray_to_strings(specs) 414 415 def push(self, specs, callbacks=None): 416 """Push the given refspec to the remote. Raises ``GitError`` on 417 protocol error or unpack failure. 418 419 :param [str] specs: push refspecs to use 420 """ 421 push_opts = ffi.new('git_push_options *') 422 err = C.git_push_init_options(push_opts, C.GIT_PUSH_OPTIONS_VERSION) 423 424 if callbacks is None: 425 callbacks = RemoteCallbacks() 426 427 callbacks._fill_push_options(push_opts) 428 # Build custom callback structure 429 430 try: 431 with StrArray(specs) as refspecs: 432 err = C.git_remote_push(self._remote, refspecs, push_opts) 433 check_error(err) 434 finally: 435 callbacks._self_handle = None 436 437 def get_credentials(fn, url, username, allowed): 438 """Call fn and return the credentials object""" 439 440 url_str = maybe_string(url) 441 username_str = maybe_string(username) 442 443 creds = fn(url_str, username_str, allowed) 444 445 credential_type = getattr(creds, 'credential_type', None) 446 credential_tuple = getattr(creds, 'credential_tuple', None) 447 if not credential_type or not credential_tuple: 448 raise TypeError("credential does not implement interface") 449 450 cred_type = credential_type 451 452 if not (allowed & cred_type): 453 raise TypeError("invalid credential type") 454 455 ccred = ffi.new('git_cred **') 456 if cred_type == C.GIT_CREDTYPE_USERPASS_PLAINTEXT: 457 name, passwd = credential_tuple 458 err = C.git_cred_userpass_plaintext_new(ccred, to_bytes(name), 459 to_bytes(passwd)) 460 461 elif cred_type == C.GIT_CREDTYPE_SSH_KEY: 462 name, pubkey, privkey, passphrase = credential_tuple 463 if pubkey is None and privkey is None: 464 err = C.git_cred_ssh_key_from_agent(ccred, to_bytes(name)) 465 else: 466 err = C.git_cred_ssh_key_new(ccred, to_bytes(name), 467 to_bytes(pubkey), to_bytes(privkey), 468 to_bytes(passphrase)) 469 else: 470 raise TypeError("unsupported credential type") 471 472 check_error(err) 473 474 return ccred 475 476 class RemoteCollection(object): 477 """Collection of configured remotes 478 479 You can use this class to look up and manage the remotes configured 480 in a repository. You can access repositories using index 481 access. E.g. to look up the "origin" remote, you can use 482 483 >>> repo.remotes["origin"] 484 """ 485 486 def __init__(self, repo): 487 self._repo = repo; 488 489 def __len__(self): 490 names = ffi.new('git_strarray *') 491 492 try: 493 err = C.git_remote_list(names, self._repo._repo) 494 check_error(err) 495 496 return names.count 497 finally: 498 C.git_strarray_free(names) 499 500 def __iter__(self): 501 names = ffi.new('git_strarray *') 502 503 try: 504 err = C.git_remote_list(names, self._repo._repo) 505 check_error(err) 506 507 cremote = ffi.new('git_remote **') 508 for i in range(names.count): 509 err = C.git_remote_lookup(cremote, self._repo._repo, names.strings[i]) 510 check_error(err) 511 512 yield Remote(self._repo, cremote[0]) 513 finally: 514 C.git_strarray_free(names) 515 516 def __getitem__(self, name): 517 if isinstance(name, int): 518 return list(self)[name] 519 520 cremote = ffi.new('git_remote **') 521 err = C.git_remote_lookup(cremote, self._repo._repo, to_bytes(name)) 522 check_error(err) 523 524 return Remote(self._repo, cremote[0]) 525 526 def create(self, name, url, fetch=None): 527 """Create a new remote with the given name and url. Returns a <Remote> 528 object. 529 530 If 'fetch' is provided, this fetch refspec will be used instead of the default 531 """ 532 533 cremote = ffi.new('git_remote **') 534 535 if fetch: 536 err = C.git_remote_create_with_fetchspec(cremote, self._repo._repo, to_bytes(name), to_bytes(url), to_bytes(fetch)) 537 else: 538 err = C.git_remote_create(cremote, self._repo._repo, to_bytes(name), to_bytes(url)) 539 540 check_error(err) 541 542 return Remote(self._repo, cremote[0]) 543 544 def rename(self, name, new_name): 545 """Rename a remote in the configuration. The refspecs in standard 546 format will be renamed. 547 548 Returns a list of fetch refspecs (list of strings) which were not in 549 the standard format and thus could not be remapped. 550 """ 551 552 if not new_name: 553 raise ValueError("Current remote name must be a non-empty string") 554 555 if not new_name: 556 raise ValueError("New remote name must be a non-empty string") 557 558 problems = ffi.new('git_strarray *') 559 err = C.git_remote_rename(problems, self._repo._repo, to_bytes(name), to_bytes(new_name)) 560 check_error(err) 561 562 ret = strarray_to_strings(problems) 563 C.git_strarray_free(problems) 564 565 return ret 566 567 def delete(self, name): 568 """Remove a remote from the configuration 569 570 All remote-tracking branches and configuration settings for the remote will be removed. 571 """ 572 err = C.git_remote_delete(self._repo._repo, to_bytes(name)) 573 check_error(err) 574 575 def set_url(self, name, url): 576 """ Set the URL for a remote 577 """ 578 err = C.git_remote_set_url(self._repo._repo, to_bytes(name), to_bytes(url)) 579 check_error(err) 580 581 def set_push_url(self, name, url): 582 """Set the push-URL for a remote 583 """ 584 err = C.git_remote_set_pushurl(self._repo._repo, to_bytes(name), to_bytes(url)) 585 check_error(err) 586 587 def add_fetch(self, name, refspec): 588 """Add a fetch refspec (str) to the remote 589 """ 590 591 err = C.git_remote_add_fetch(self._repo._repo, to_bytes(name), to_bytes(refspec)) 592 check_error(err) 593 594 def add_push(self, name, refspec): 595 """Add a push refspec (str) to the remote 596 """ 597 598 err = C.git_remote_add_push(self._repo._repo, to_bytes(name), to_bytes(refspec)) 599 check_error(err)