Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/remote.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

468 statements  

1# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 

2# 

3# This module is part of GitPython and is released under the 

4# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 

5 

6"""Module implementing a remote object allowing easy access to git remotes.""" 

7 

8__all__ = ["RemoteProgress", "PushInfo", "FetchInfo", "Remote"] 

9 

10import contextlib 

11import logging 

12import re 

13 

14from git.cmd import Git, handle_process_output 

15from git.compat import defenc, force_text 

16from git.config import GitConfigParser, SectionConstraint, cp 

17from git.exc import GitCommandError, UnsafeOptionError 

18from git.refs import Head, Reference, RemoteReference, SymbolicReference, TagReference 

19from git.util import ( 

20 CallableRemoteProgress, 

21 IterableList, 

22 IterableObj, 

23 LazyMixin, 

24 RemoteProgress, 

25 join_path, 

26) 

27 

28# typing------------------------------------------------------- 

29 

30from typing import ( 

31 Any, 

32 Callable, 

33 Dict, 

34 Iterator, 

35 List, 

36 NoReturn, 

37 Optional, 

38 Sequence, 

39 TYPE_CHECKING, 

40 Type, 

41 TypeVar, 

42 Union, 

43 cast, 

44 overload, 

45) 

46 

47from git.types import AnyGitObject, Literal, PathLike 

48 

49if TYPE_CHECKING: 

50 from git.objects.commit import Commit 

51 from git.objects.submodule.base import UpdateProgress 

52 from git.repo.base import Repo 

53 

54_T_RemoteName = TypeVar("_T_RemoteName", bound=Union[str, "Remote"]) 

55 

56flagKeyLiteral = Literal[" ", "!", "+", "-", "*", "=", "t", "?"] 

57 

58# ------------------------------------------------------------- 

59 

60_logger = logging.getLogger(__name__) 

61 

62# { Utilities 

63 

64 

65def add_progress( 

66 kwargs: Any, 

67 git: Git, 

68 progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None], 

69) -> Any: 

70 """Add the ``--progress`` flag to the given `kwargs` dict if supported by the git 

71 command. 

72 

73 :note: 

74 If the actual progress in the given progress instance is not given, we do not 

75 request any progress. 

76 

77 :return: 

78 Possibly altered `kwargs` 

79 """ 

80 if progress is not None: 

81 v = git.version_info[:2] 

82 if v >= (1, 7): 

83 kwargs["progress"] = True 

84 # END handle --progress 

85 # END handle progress 

86 return kwargs 

87 

88 

89# } END utilities 

90 

91 

92@overload 

93def to_progress_instance(progress: None) -> RemoteProgress: ... 

94 

95 

96@overload 

97def to_progress_instance(progress: Callable[..., Any]) -> CallableRemoteProgress: ... 

98 

99 

100@overload 

101def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: ... 

102 

103 

104def to_progress_instance( 

105 progress: Union[Callable[..., Any], RemoteProgress, None], 

106) -> Union[RemoteProgress, CallableRemoteProgress]: 

107 """Given the `progress` return a suitable object derived from 

108 :class:`~git.util.RemoteProgress`.""" 

109 # New API only needs progress as a function. 

110 if callable(progress): 

111 return CallableRemoteProgress(progress) 

112 

113 # Where None is passed create a parser that eats the progress. 

114 elif progress is None: 

115 return RemoteProgress() 

116 

117 # Assume its the old API with an instance of RemoteProgress. 

118 return progress 

119 

120 

121class PushInfo(IterableObj): 

122 """ 

123 Carries information about the result of a push operation of a single head:: 

124 

125 info = remote.push()[0] 

126 info.flags # bitflags providing more information about the result 

127 info.local_ref # Reference pointing to the local reference that was pushed 

128 # It is None if the ref was deleted. 

129 info.remote_ref_string # path to the remote reference located on the remote side 

130 info.remote_ref # Remote Reference on the local side corresponding to 

131 # the remote_ref_string. It can be a TagReference as well. 

132 info.old_commit # commit at which the remote_ref was standing before we pushed 

133 # it to local_ref.commit. Will be None if an error was indicated 

134 info.summary # summary line providing human readable english text about the push 

135 """ 

136 

137 __slots__ = ( 

138 "local_ref", 

139 "remote_ref_string", 

140 "flags", 

141 "_old_commit_sha", 

142 "_remote", 

143 "summary", 

144 ) 

145 

146 _id_attribute_ = "pushinfo" 

147 

148 ( 

149 NEW_TAG, 

150 NEW_HEAD, 

151 NO_MATCH, 

152 REJECTED, 

153 REMOTE_REJECTED, 

154 REMOTE_FAILURE, 

155 DELETED, 

156 FORCED_UPDATE, 

157 FAST_FORWARD, 

158 UP_TO_DATE, 

159 ERROR, 

160 ) = [1 << x for x in range(11)] 

161 

162 _flag_map = { 

163 "X": NO_MATCH, 

164 "-": DELETED, 

165 "*": 0, 

166 "+": FORCED_UPDATE, 

167 " ": FAST_FORWARD, 

168 "=": UP_TO_DATE, 

169 "!": ERROR, 

170 } 

171 

172 def __init__( 

173 self, 

174 flags: int, 

175 local_ref: Union[SymbolicReference, None], 

176 remote_ref_string: str, 

177 remote: "Remote", 

178 old_commit: Optional[str] = None, 

179 summary: str = "", 

180 ) -> None: 

181 """Initialize a new instance. 

182 

183 local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None 

184 """ 

185 self.flags = flags 

186 self.local_ref = local_ref 

187 self.remote_ref_string = remote_ref_string 

188 self._remote = remote 

189 self._old_commit_sha = old_commit 

190 self.summary = summary 

191 

192 @property 

193 def old_commit(self) -> Union["Commit", None]: 

194 return self._old_commit_sha and self._remote.repo.commit(self._old_commit_sha) or None 

195 

196 @property 

197 def remote_ref(self) -> Union[RemoteReference, TagReference]: 

198 """ 

199 :return: 

200 Remote :class:`~git.refs.reference.Reference` or 

201 :class:`~git.refs.tag.TagReference` in the local repository corresponding to 

202 the :attr:`remote_ref_string` kept in this instance. 

203 """ 

204 # Translate heads to a local remote. Tags stay as they are. 

205 if self.remote_ref_string.startswith("refs/tags"): 

206 return TagReference(self._remote.repo, self.remote_ref_string) 

207 elif self.remote_ref_string.startswith("refs/heads"): 

208 remote_ref = Reference(self._remote.repo, self.remote_ref_string) 

209 return RemoteReference( 

210 self._remote.repo, 

211 "refs/remotes/%s/%s" % (str(self._remote), remote_ref.name), 

212 ) 

213 else: 

214 raise ValueError("Could not handle remote ref: %r" % self.remote_ref_string) 

215 # END 

216 

217 @classmethod 

218 def _from_line(cls, remote: "Remote", line: str) -> "PushInfo": 

219 """Create a new :class:`PushInfo` instance as parsed from line which is expected 

220 to be like refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes.""" 

221 control_character, from_to, summary = line.split("\t", 3) 

222 flags = 0 

223 

224 # Control character handling 

225 try: 

226 flags |= cls._flag_map[control_character] 

227 except KeyError as e: 

228 raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e 

229 # END handle control character 

230 

231 # from_to handling 

232 from_ref_string, to_ref_string = from_to.split(":") 

233 if flags & cls.DELETED: 

234 from_ref: Union[SymbolicReference, None] = None 

235 else: 

236 if from_ref_string == "(delete)": 

237 from_ref = None 

238 else: 

239 from_ref = Reference.from_path(remote.repo, from_ref_string) 

240 

241 # Commit handling, could be message or commit info 

242 old_commit: Optional[str] = None 

243 if summary.startswith("["): 

244 if "[rejected]" in summary: 

245 flags |= cls.REJECTED 

246 elif "[remote rejected]" in summary: 

247 flags |= cls.REMOTE_REJECTED 

248 elif "[remote failure]" in summary: 

249 flags |= cls.REMOTE_FAILURE 

250 elif "[no match]" in summary: 

251 flags |= cls.ERROR 

252 elif "[new tag]" in summary: 

253 flags |= cls.NEW_TAG 

254 elif "[new branch]" in summary: 

255 flags |= cls.NEW_HEAD 

256 # `uptodate` encoded in control character 

257 else: 

258 # Fast-forward or forced update - was encoded in control character, 

259 # but we parse the old and new commit. 

260 split_token = "..." 

261 if control_character == " ": 

262 split_token = ".." 

263 old_sha, _new_sha = summary.split(" ")[0].split(split_token) 

264 # Have to use constructor here as the sha usually is abbreviated. 

265 old_commit = old_sha 

266 # END message handling 

267 

268 return PushInfo(flags, from_ref, to_ref_string, remote, old_commit, summary) 

269 

270 @classmethod 

271 def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['PushInfo']: 

272 raise NotImplementedError 

273 

274 

275class PushInfoList(IterableList[PushInfo]): 

276 """:class:`~git.util.IterableList` of :class:`PushInfo` objects.""" 

277 

278 def __new__(cls) -> "PushInfoList": 

279 return cast(PushInfoList, IterableList.__new__(cls, "push_infos")) 

280 

281 def __init__(self) -> None: 

282 super().__init__("push_infos") 

283 self.error: Optional[Exception] = None 

284 

285 def raise_if_error(self) -> None: 

286 """Raise an exception if any ref failed to push.""" 

287 if self.error: 

288 raise self.error 

289 

290 

291class FetchInfo(IterableObj): 

292 """ 

293 Carries information about the results of a fetch operation of a single head:: 

294 

295 info = remote.fetch()[0] 

296 info.ref # Symbolic Reference or RemoteReference to the changed 

297 # remote head or FETCH_HEAD 

298 info.flags # additional flags to be & with enumeration members, 

299 # i.e. info.flags & info.REJECTED 

300 # is 0 if ref is SymbolicReference 

301 info.note # additional notes given by git-fetch intended for the user 

302 info.old_commit # if info.flags & info.FORCED_UPDATE|info.FAST_FORWARD, 

303 # field is set to the previous location of ref, otherwise None 

304 info.remote_ref_path # The path from which we fetched on the remote. It's the remote's version of our info.ref 

305 """ 

306 

307 __slots__ = ("ref", "old_commit", "flags", "note", "remote_ref_path") 

308 

309 _id_attribute_ = "fetchinfo" 

310 

311 ( 

312 NEW_TAG, 

313 NEW_HEAD, 

314 HEAD_UPTODATE, 

315 TAG_UPDATE, 

316 REJECTED, 

317 FORCED_UPDATE, 

318 FAST_FORWARD, 

319 ERROR, 

320 ) = [1 << x for x in range(8)] 

321 

322 _re_fetch_result = re.compile(r"^ *(?:.{0,3})(.) (\[[\w \.$@]+\]|[\w\.$@]+) +(.+) -> ([^ ]+)( \(.*\)?$)?") 

323 

324 _flag_map: Dict[flagKeyLiteral, int] = { 

325 "!": ERROR, 

326 "+": FORCED_UPDATE, 

327 "*": 0, 

328 "=": HEAD_UPTODATE, 

329 " ": FAST_FORWARD, 

330 "-": TAG_UPDATE, 

331 } 

332 

333 @classmethod 

334 def refresh(cls) -> Literal[True]: 

335 """Update information about which :manpage:`git-fetch(1)` flags are supported 

336 by the git executable being used. 

337 

338 Called by the :func:`git.refresh` function in the top level ``__init__``. 

339 """ 

340 # Clear the old values in _flag_map. 

341 with contextlib.suppress(KeyError): 

342 del cls._flag_map["t"] 

343 with contextlib.suppress(KeyError): 

344 del cls._flag_map["-"] 

345 

346 # Set the value given the git version. 

347 if Git().version_info[:2] >= (2, 10): 

348 cls._flag_map["t"] = cls.TAG_UPDATE 

349 else: 

350 cls._flag_map["-"] = cls.TAG_UPDATE 

351 

352 return True 

353 

354 def __init__( 

355 self, 

356 ref: SymbolicReference, 

357 flags: int, 

358 note: str = "", 

359 old_commit: Union[AnyGitObject, None] = None, 

360 remote_ref_path: Optional[PathLike] = None, 

361 ) -> None: 

362 """Initialize a new instance.""" 

363 self.ref = ref 

364 self.flags = flags 

365 self.note = note 

366 self.old_commit = old_commit 

367 self.remote_ref_path = remote_ref_path 

368 

369 def __str__(self) -> str: 

370 return self.name 

371 

372 @property 

373 def name(self) -> str: 

374 """:return: Name of our remote ref""" 

375 return self.ref.name 

376 

377 @property 

378 def commit(self) -> "Commit": 

379 """:return: Commit of our remote ref""" 

380 return self.ref.commit 

381 

382 @classmethod 

383 def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo": 

384 """Parse information from the given line as returned by ``git-fetch -v`` and 

385 return a new :class:`FetchInfo` object representing this information. 

386 

387 We can handle a line as follows:: 

388 

389 %c %-*s %-*s -> %s%s 

390 

391 Where ``c`` is either a space, ``!``, ``+``, ``-``, ``*``, or ``=``: 

392 

393 - '!' means error 

394 - '+' means success forcing update 

395 - '-' means a tag was updated 

396 - '*' means birth of new branch or tag 

397 - '=' means the head was up to date (and not moved) 

398 - ' ' means a fast-forward 

399 

400 `fetch_line` is the corresponding line from FETCH_HEAD, like:: 

401 

402 acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo 

403 """ 

404 match = cls._re_fetch_result.match(line) 

405 if match is None: 

406 raise ValueError("Failed to parse line: %r" % line) 

407 

408 # Parse lines. 

409 remote_local_ref_str: str 

410 ( 

411 control_character, 

412 operation, 

413 local_remote_ref, 

414 remote_local_ref_str, 

415 note, 

416 ) = match.groups() 

417 control_character = cast(flagKeyLiteral, control_character) 

418 try: 

419 _new_hex_sha, _fetch_operation, fetch_note = fetch_line.split("\t") 

420 ref_type_name, fetch_note = fetch_note.split(" ", 1) 

421 except ValueError as e: # unpack error 

422 raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e 

423 

424 # Parse flags from control_character. 

425 flags = 0 

426 try: 

427 flags |= cls._flag_map[control_character] 

428 except KeyError as e: 

429 raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e 

430 # END control char exception handling 

431 

432 # Parse operation string for more info. 

433 # This makes no sense for symbolic refs, but we parse it anyway. 

434 old_commit: Union[AnyGitObject, None] = None 

435 is_tag_operation = False 

436 if "rejected" in operation: 

437 flags |= cls.REJECTED 

438 if "new tag" in operation: 

439 flags |= cls.NEW_TAG 

440 is_tag_operation = True 

441 if "tag update" in operation: 

442 flags |= cls.TAG_UPDATE 

443 is_tag_operation = True 

444 if "new branch" in operation: 

445 flags |= cls.NEW_HEAD 

446 if "..." in operation or ".." in operation: 

447 split_token = "..." 

448 if control_character == " ": 

449 split_token = split_token[:-1] 

450 old_commit = repo.rev_parse(operation.split(split_token)[0]) 

451 # END handle refspec 

452 

453 # Handle FETCH_HEAD and figure out ref type. 

454 # If we do not specify a target branch like master:refs/remotes/origin/master, 

455 # the fetch result is stored in FETCH_HEAD which destroys the rule we usually 

456 # have. In that case we use a symbolic reference which is detached. 

457 ref_type: Optional[Type[SymbolicReference]] = None 

458 if remote_local_ref_str == "FETCH_HEAD": 

459 ref_type = SymbolicReference 

460 elif ref_type_name == "tag" or is_tag_operation: 

461 # The ref_type_name can be branch, whereas we are still seeing a tag 

462 # operation. It happens during testing, which is based on actual git 

463 # operations. 

464 ref_type = TagReference 

465 elif ref_type_name in ("remote-tracking", "branch"): 

466 # Note: remote-tracking is just the first part of the 

467 # 'remote-tracking branch' token. We don't parse it correctly, but it's 

468 # enough to know what to do, and it's new in git 1.7something. 

469 ref_type = RemoteReference 

470 elif "/" in ref_type_name: 

471 # If the fetch spec look something like '+refs/pull/*:refs/heads/pull/*', 

472 # and is thus pretty much anything the user wants, we will have trouble 

473 # determining what's going on. For now, we assume the local ref is a Head. 

474 ref_type = Head 

475 else: 

476 raise TypeError("Cannot handle reference type: %r" % ref_type_name) 

477 # END handle ref type 

478 

479 # Create ref instance. 

480 if ref_type is SymbolicReference: 

481 remote_local_ref = ref_type(repo, "FETCH_HEAD") 

482 else: 

483 # Determine prefix. Tags are usually pulled into refs/tags; they may have 

484 # subdirectories. It is not clear sometimes where exactly the item is, 

485 # unless we have an absolute path as indicated by the 'ref/' prefix. 

486 # Otherwise even a tag could be in refs/remotes, which is when it will have 

487 # the 'tags/' subdirectory in its path. We don't want to test for actual 

488 # existence, but try to figure everything out analytically. 

489 ref_path: Optional[PathLike] = None 

490 remote_local_ref_str = remote_local_ref_str.strip() 

491 

492 if remote_local_ref_str.startswith(Reference._common_path_default + "/"): 

493 # Always use actual type if we get absolute paths. This will always be 

494 # the case if something is fetched outside of refs/remotes (if its not a 

495 # tag). 

496 ref_path = remote_local_ref_str 

497 if ref_type is not TagReference and not remote_local_ref_str.startswith( 

498 RemoteReference._common_path_default + "/" 

499 ): 

500 ref_type = Reference 

501 # END downgrade remote reference 

502 elif ref_type is TagReference and "tags/" in remote_local_ref_str: 

503 # Even though it's a tag, it is located in refs/remotes. 

504 ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str) 

505 else: 

506 ref_path = join_path(ref_type._common_path_default, remote_local_ref_str) 

507 # END obtain refpath 

508 

509 # Even though the path could be within the git conventions, we make sure we 

510 # respect whatever the user wanted, and disabled path checking. 

511 remote_local_ref = ref_type(repo, ref_path, check_path=False) 

512 # END create ref instance 

513 

514 note = (note and note.strip()) or "" 

515 

516 return cls(remote_local_ref, flags, note, old_commit, local_remote_ref) 

517 

518 @classmethod 

519 def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> Iterator['FetchInfo']: 

520 raise NotImplementedError 

521 

522 

523Progress = Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None] 

524 

525 

526class Remote(LazyMixin, IterableObj): 

527 """Provides easy read and write access to a git remote. 

528 

529 Everything not part of this interface is considered an option for the current 

530 remote, allowing constructs like ``remote.pushurl`` to query the pushurl. 

531 

532 :note: 

533 When querying configuration, the configuration accessor will be cached to speed 

534 up subsequent accesses. 

535 """ 

536 

537 __slots__ = ("repo", "name", "_config_reader") 

538 

539 _id_attribute_ = "name" 

540 

541 unsafe_git_fetch_options = [ 

542 # This option allows users to execute arbitrary commands. 

543 # https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---upload-packltupload-packgt 

544 "--upload-pack", 

545 ] 

546 unsafe_git_pull_options = [ 

547 # This option allows users to execute arbitrary commands. 

548 # https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---upload-packltupload-packgt 

549 "--upload-pack" 

550 ] 

551 unsafe_git_push_options = [ 

552 # This option allows users to execute arbitrary commands. 

553 # https://git-scm.com/docs/git-push#Documentation/git-push.txt---execltgit-receive-packgt 

554 "--receive-pack", 

555 "--exec", 

556 ] 

557 

558 url: str # Obtained dynamically from _config_reader. See __getattr__ below. 

559 """The URL configured for the remote.""" 

560 

561 def __init__(self, repo: "Repo", name: str) -> None: 

562 """Initialize a remote instance. 

563 

564 :param repo: 

565 The repository we are a remote of. 

566 

567 :param name: 

568 The name of the remote, e.g. ``origin``. 

569 """ 

570 self.repo = repo 

571 self.name = name 

572 

573 def __getattr__(self, attr: str) -> Any: 

574 """Allows to call this instance like ``remote.special(*args, **kwargs)`` to 

575 call ``git remote special self.name``.""" 

576 if attr == "_config_reader": 

577 return super().__getattr__(attr) 

578 

579 # Sometimes, probably due to a bug in Python itself, we are being called even 

580 # though a slot of the same name exists. 

581 try: 

582 return self._config_reader.get(attr) 

583 except cp.NoOptionError: 

584 return super().__getattr__(attr) 

585 # END handle exception 

586 

587 def _config_section_name(self) -> str: 

588 return 'remote "%s"' % self.name 

589 

590 def _set_cache_(self, attr: str) -> None: 

591 if attr == "_config_reader": 

592 # NOTE: This is cached as __getattr__ is overridden to return remote config 

593 # values implicitly, such as in print(r.pushurl). 

594 self._config_reader = SectionConstraint( 

595 self.repo.config_reader("repository"), 

596 self._config_section_name(), 

597 ) 

598 else: 

599 super()._set_cache_(attr) 

600 

601 def __str__(self) -> str: 

602 return self.name 

603 

604 def __repr__(self) -> str: 

605 return '<git.%s "%s">' % (self.__class__.__name__, self.name) 

606 

607 def __eq__(self, other: object) -> bool: 

608 return isinstance(other, type(self)) and self.name == other.name 

609 

610 def __ne__(self, other: object) -> bool: 

611 return not (self == other) 

612 

613 def __hash__(self) -> int: 

614 return hash(self.name) 

615 

616 def exists(self) -> bool: 

617 """ 

618 :return: 

619 ``True`` if this is a valid, existing remote. 

620 Valid remotes have an entry in the repository's configuration. 

621 """ 

622 try: 

623 self.config_reader.get("url") 

624 return True 

625 except cp.NoOptionError: 

626 # We have the section at least... 

627 return True 

628 except cp.NoSectionError: 

629 return False 

630 

631 @classmethod 

632 def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote"]: 

633 """:return: Iterator yielding :class:`Remote` objects of the given repository""" 

634 for section in repo.config_reader("repository").sections(): 

635 if not section.lower().startswith("remote "): 

636 continue 

637 lbound = section.find('"') 

638 rbound = section.rfind('"') 

639 if lbound == -1 or rbound == -1: 

640 raise ValueError("Remote-Section has invalid format: %r" % section) 

641 yield Remote(repo, section[lbound + 1 : rbound]) 

642 # END for each configuration section 

643 

644 def set_url( 

645 self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any 

646 ) -> "Remote": 

647 """Configure URLs on current remote (cf. command ``git remote set-url``). 

648 

649 This command manages URLs on the remote. 

650 

651 :param new_url: 

652 String being the URL to add as an extra remote URL. 

653 

654 :param old_url: 

655 When set, replaces this URL with `new_url` for the remote. 

656 

657 :param allow_unsafe_protocols: 

658 Allow unsafe protocols to be used, like ``ext``. 

659 

660 :return: 

661 self 

662 """ 

663 if not allow_unsafe_protocols: 

664 Git.check_unsafe_protocols(new_url) 

665 scmd = "set-url" 

666 kwargs["insert_kwargs_after"] = scmd 

667 if old_url: 

668 self.repo.git.remote(scmd, "--", self.name, new_url, old_url, **kwargs) 

669 else: 

670 self.repo.git.remote(scmd, "--", self.name, new_url, **kwargs) 

671 return self 

672 

673 def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote": 

674 """Adds a new url on current remote (special case of ``git remote set-url``). 

675 

676 This command adds new URLs to a given remote, making it possible to have 

677 multiple URLs for a single remote. 

678 

679 :param url: 

680 String being the URL to add as an extra remote URL. 

681 

682 :param allow_unsafe_protocols: 

683 Allow unsafe protocols to be used, like ``ext``. 

684 

685 :return: 

686 self 

687 """ 

688 return self.set_url(url, add=True, allow_unsafe_protocols=allow_unsafe_protocols) 

689 

690 def delete_url(self, url: str, **kwargs: Any) -> "Remote": 

691 """Deletes a new url on current remote (special case of ``git remote set-url``). 

692 

693 This command deletes new URLs to a given remote, making it possible to have 

694 multiple URLs for a single remote. 

695 

696 :param url: 

697 String being the URL to delete from the remote. 

698 

699 :return: 

700 self 

701 """ 

702 return self.set_url(url, delete=True) 

703 

704 @property 

705 def urls(self) -> Iterator[str]: 

706 """:return: Iterator yielding all configured URL targets on a remote as strings""" 

707 try: 

708 remote_details = self.repo.git.remote("get-url", "--all", self.name) 

709 assert isinstance(remote_details, str) 

710 for line in remote_details.split("\n"): 

711 yield line 

712 except GitCommandError as ex: 

713 ## We are on git < 2.7 (i.e TravisCI as of Oct-2016), 

714 # so `get-utl` command does not exist yet! 

715 # see: https://github.com/gitpython-developers/GitPython/pull/528#issuecomment-252976319 

716 # and: http://stackoverflow.com/a/32991784/548792 

717 # 

718 if "Unknown subcommand: get-url" in str(ex): 

719 try: 

720 remote_details = self.repo.git.remote("show", self.name) 

721 assert isinstance(remote_details, str) 

722 for line in remote_details.split("\n"): 

723 if " Push URL:" in line: 

724 yield line.split(": ")[-1] 

725 except GitCommandError as _ex: 

726 if any(msg in str(_ex) for msg in ["correct access rights", "cannot run ssh"]): 

727 # If ssh is not setup to access this repository, see issue 694. 

728 remote_details = self.repo.git.config("--get-all", "remote.%s.url" % self.name) 

729 assert isinstance(remote_details, str) 

730 for line in remote_details.split("\n"): 

731 yield line 

732 else: 

733 raise _ex 

734 else: 

735 raise ex 

736 

737 @property 

738 def refs(self) -> IterableList[RemoteReference]: 

739 """ 

740 :return: 

741 :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference` 

742 objects. 

743 

744 It is prefixed, allowing you to omit the remote path portion, e.g.:: 

745 

746 remote.refs.master # yields RemoteReference('/refs/remotes/origin/master') 

747 """ 

748 out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) 

749 out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name)) 

750 return out_refs 

751 

752 @property 

753 def stale_refs(self) -> IterableList[Reference]: 

754 """ 

755 :return: 

756 :class:`~git.util.IterableList` of :class:`~git.refs.remote.RemoteReference` 

757 objects that do not have a corresponding head in the remote reference 

758 anymore as they have been deleted on the remote side, but are still 

759 available locally. 

760 

761 The :class:`~git.util.IterableList` is prefixed, hence the 'origin' must be 

762 omitted. See :attr:`refs` property for an example. 

763 

764 To make things more complicated, it can be possible for the list to include 

765 other kinds of references, for example, tag references, if these are stale 

766 as well. This is a fix for the issue described here: 

767 https://github.com/gitpython-developers/GitPython/issues/260 

768 """ 

769 out_refs: IterableList[Reference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) 

770 for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: 

771 # expecting 

772 # * [would prune] origin/new_branch 

773 token = " * [would prune] " 

774 if not line.startswith(token): 

775 continue 

776 ref_name = line.replace(token, "") 

777 # Sometimes, paths start with a full ref name, like refs/tags/foo. See #260. 

778 if ref_name.startswith(Reference._common_path_default + "/"): 

779 out_refs.append(Reference.from_path(self.repo, ref_name)) 

780 else: 

781 fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name) 

782 out_refs.append(RemoteReference(self.repo, fqhn)) 

783 # END special case handling 

784 # END for each line 

785 return out_refs 

786 

787 @classmethod 

788 def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote": 

789 """Create a new remote to the given repository. 

790 

791 :param repo: 

792 Repository instance that is to receive the new remote. 

793 

794 :param name: 

795 Desired name of the remote. 

796 

797 :param url: 

798 URL which corresponds to the remote's name. 

799 

800 :param allow_unsafe_protocols: 

801 Allow unsafe protocols to be used, like ``ext``. 

802 

803 :param kwargs: 

804 Additional arguments to be passed to the ``git remote add`` command. 

805 

806 :return: 

807 New :class:`Remote` instance 

808 

809 :raise git.exc.GitCommandError: 

810 In case an origin with that name already exists. 

811 """ 

812 scmd = "add" 

813 kwargs["insert_kwargs_after"] = scmd 

814 url = Git.polish_url(url, expand_vars=False) 

815 if not allow_unsafe_protocols: 

816 Git.check_unsafe_protocols(url) 

817 repo.git.remote(scmd, "--", name, url, **kwargs) 

818 return cls(repo, name) 

819 

820 # `add` is an alias. 

821 @classmethod 

822 def add(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote": 

823 return cls.create(repo, name, url, **kwargs) 

824 

825 @classmethod 

826 def remove(cls, repo: "Repo", name: _T_RemoteName) -> _T_RemoteName: 

827 """Remove the remote with the given name. 

828 

829 :return: 

830 The passed remote name to remove 

831 """ 

832 repo.git.remote("rm", name) 

833 remote = name 

834 if isinstance(remote, cls): 

835 remote._clear_cache() 

836 return name 

837 

838 @classmethod 

839 def rm(cls, repo: "Repo", name: _T_RemoteName) -> _T_RemoteName: 

840 """Alias of remove. 

841 Remove the remote with the given name. 

842 

843 :return: 

844 The passed remote name to remove 

845 """ 

846 return cls.remove(repo, name) 

847 

848 def rename(self, new_name: str) -> "Remote": 

849 """Rename self to the given `new_name`. 

850 

851 :return: 

852 self 

853 """ 

854 if self.name == new_name: 

855 return self 

856 

857 self.repo.git.remote("rename", self.name, new_name) 

858 self.name = new_name 

859 self._clear_cache() 

860 

861 return self 

862 

863 def update(self, **kwargs: Any) -> "Remote": 

864 """Fetch all changes for this remote, including new branches which will be 

865 forced in (in case your local remote branch is not part the new remote branch's 

866 ancestry anymore). 

867 

868 :param kwargs: 

869 Additional arguments passed to ``git remote update``. 

870 

871 :return: 

872 self 

873 """ 

874 # Like pull, remote update forwards operands to fetch without `--`. 

875 if self.name.startswith("-"): 

876 raise UnsafeOptionError("Remote names used by update must not start with '-'.") 

877 scmd = "update" 

878 kwargs["insert_kwargs_after"] = scmd 

879 self.repo.git.remote(scmd, self.name, **kwargs) 

880 return self 

881 

882 def _get_fetch_info_from_stderr( 

883 self, 

884 proc: "Git.AutoInterrupt", 

885 progress: Progress, 

886 kill_after_timeout: Union[None, float] = None, 

887 ) -> IterableList["FetchInfo"]: 

888 progress = to_progress_instance(progress) 

889 

890 # Skip first line as it is some remote info we are not interested in. 

891 output: IterableList["FetchInfo"] = IterableList("name") 

892 

893 # Lines which are no progress are fetch info lines. 

894 # This also waits for the command to finish. 

895 # Skip some progress lines that don't provide relevant information. 

896 fetch_info_lines = [] 

897 # Basically we want all fetch info lines which appear to be in regular form, and 

898 # thus have a command character. Everything else we ignore. 

899 cmds = set(FetchInfo._flag_map.keys()) 

900 

901 progress_handler = progress.new_message_handler() 

902 handle_process_output( 

903 proc, 

904 None, 

905 progress_handler, 

906 finalizer=None, 

907 decode_streams=False, 

908 kill_after_timeout=kill_after_timeout, 

909 ) 

910 

911 stderr_text = "\n".join(progress.error_lines) 

912 proc.wait(stderr=stderr_text) 

913 if stderr_text: 

914 _logger.warning("Error lines received while fetching: %s", stderr_text) 

915 

916 for line in progress.other_lines: 

917 line = force_text(line) 

918 for cmd in cmds: 

919 if len(line) > 1 and line[0] == " " and line[1] == cmd: 

920 fetch_info_lines.append(line) 

921 continue 

922 

923 # Read head information. 

924 fetch_head = SymbolicReference(self.repo, "FETCH_HEAD") 

925 with open(fetch_head.abspath, "rb") as fp: 

926 fetch_head_info = [line.decode(defenc) for line in fp.readlines()] 

927 

928 l_fil = len(fetch_info_lines) 

929 l_fhi = len(fetch_head_info) 

930 if l_fil != l_fhi: 

931 msg = "Fetch head lines do not match lines provided via progress information\n" 

932 msg += "length of progress lines %i should be equal to lines in FETCH_HEAD file %i\n" 

933 msg += "Will ignore extra progress lines or fetch head lines." 

934 msg %= (l_fil, l_fhi) 

935 _logger.debug(msg) 

936 _logger.debug(b"info lines: " + str(fetch_info_lines).encode("UTF-8")) 

937 _logger.debug(b"head info: " + str(fetch_head_info).encode("UTF-8")) 

938 if l_fil < l_fhi: 

939 fetch_head_info = fetch_head_info[:l_fil] 

940 else: 

941 fetch_info_lines = fetch_info_lines[:l_fhi] 

942 # END truncate correct list 

943 # END sanity check + sanitization 

944 

945 for err_line, fetch_line in zip(fetch_info_lines, fetch_head_info): 

946 try: 

947 output.append(FetchInfo._from_line(self.repo, err_line, fetch_line)) 

948 except ValueError as exc: 

949 _logger.debug("Caught error while parsing line: %s", exc) 

950 _logger.warning("Git informed while fetching: %s", err_line.strip()) 

951 return output 

952 

953 def _get_push_info( 

954 self, 

955 proc: "Git.AutoInterrupt", 

956 progress: Union[Callable[..., Any], RemoteProgress, None], 

957 kill_after_timeout: Union[None, float] = None, 

958 ) -> PushInfoList: 

959 progress = to_progress_instance(progress) 

960 

961 # Read progress information from stderr. 

962 # We hope stdout can hold all the data, it should... 

963 # Read the lines manually as it will use carriage returns between the messages 

964 # to override the previous one. This is why we read the bytes manually. 

965 progress_handler = progress.new_message_handler() 

966 output: PushInfoList = PushInfoList() 

967 

968 def stdout_handler(line: str) -> None: 

969 try: 

970 output.append(PushInfo._from_line(self, line)) 

971 except ValueError: 

972 # If an error happens, additional info is given which we parse below. 

973 pass 

974 

975 handle_process_output( 

976 proc, 

977 stdout_handler, 

978 progress_handler, 

979 finalizer=None, 

980 decode_streams=False, 

981 kill_after_timeout=kill_after_timeout, 

982 ) 

983 stderr_text = "\n".join(progress.error_lines) 

984 try: 

985 proc.wait(stderr=stderr_text) 

986 except Exception as e: 

987 # This is different than fetch (which fails if there is any stderr 

988 # even if there is an output). 

989 if not output: 

990 raise 

991 elif stderr_text or proc._timeout_error: 

992 if stderr_text: 

993 _logger.warning("Error lines received while pushing: %s", stderr_text) 

994 output.error = e 

995 

996 return output 

997 

998 def _assert_refspec(self) -> None: 

999 """Turns out we can't deal with remotes if the refspec is missing.""" 

1000 config = self.config_reader 

1001 unset = "placeholder" 

1002 try: 

1003 if config.get_value("fetch", default=unset) is unset: 

1004 msg = "Remote '%s' has no refspec set.\n" 

1005 msg += "You can set it as follows:" 

1006 msg += " 'git config --add \"remote.%s.fetch +refs/heads/*:refs/heads/*\"'." 

1007 raise AssertionError(msg % (self.name, self.name)) 

1008 finally: 

1009 config.release() 

1010 

1011 def fetch( 

1012 self, 

1013 refspec: Union[str, List[str], None] = None, 

1014 progress: Progress = None, 

1015 verbose: bool = True, 

1016 kill_after_timeout: Union[None, float] = None, 

1017 allow_unsafe_protocols: bool = False, 

1018 allow_unsafe_options: bool = False, 

1019 **kwargs: Any, 

1020 ) -> IterableList[FetchInfo]: 

1021 """Fetch the latest changes for this remote. 

1022 

1023 :param refspec: 

1024 A "refspec" is used by fetch and push to describe the mapping 

1025 between remote ref and local ref. They are combined with a colon in 

1026 the format ``<src>:<dst>``, preceded by an optional plus sign, ``+``. 

1027 For example: ``git fetch $URL refs/heads/master:refs/heads/origin`` means 

1028 "grab the master branch head from the $URL and store it as my origin 

1029 branch head". And ``git push $URL refs/heads/master:refs/heads/to-upstream`` 

1030 means "publish my master branch head as to-upstream branch at $URL". 

1031 See also :manpage:`git-push(1)`. 

1032 

1033 Taken from the git manual, :manpage:`gitglossary(7)`. 

1034 

1035 Fetch supports multiple refspecs (as the underlying :manpage:`git-fetch(1)` 

1036 does) - supplying a list rather than a string for 'refspec' will make use of 

1037 this facility. 

1038 

1039 :param progress: 

1040 See the :meth:`push` method. 

1041 

1042 :param verbose: 

1043 Boolean for verbose output. 

1044 

1045 :param kill_after_timeout: 

1046 To specify a timeout in seconds for the git command, after which the process 

1047 should be killed. It is set to ``None`` by default. 

1048 

1049 :param allow_unsafe_protocols: 

1050 Allow unsafe protocols to be used, like ``ext``. 

1051 

1052 :param allow_unsafe_options: 

1053 Allow unsafe options to be used, like ``--upload-pack``. 

1054 

1055 :param kwargs: 

1056 Additional arguments to be passed to :manpage:`git-fetch(1)`. 

1057 

1058 :return: 

1059 IterableList(FetchInfo, ...) list of :class:`FetchInfo` instances providing 

1060 detailed information about the fetch results 

1061 

1062 :note: 

1063 As fetch does not provide progress information to non-ttys, we cannot make 

1064 it available here unfortunately as in the :meth:`push` method. 

1065 """ 

1066 if refspec is None: 

1067 # No argument refspec, then ensure the repo's config has a fetch refspec. 

1068 self._assert_refspec() 

1069 

1070 kwargs = add_progress(kwargs, self.repo.git, progress) 

1071 if isinstance(refspec, list): 

1072 args: Sequence[Optional[str]] = refspec 

1073 else: 

1074 args = [refspec] 

1075 

1076 if not allow_unsafe_protocols: 

1077 self.repo.git._check_unsafe_protocols_in_args([self, *args], kwargs) 

1078 

1079 if not allow_unsafe_options: 

1080 Git.check_unsafe_options( 

1081 options=Git._option_candidates([], kwargs), 

1082 unsafe_options=self.unsafe_git_fetch_options, 

1083 ) 

1084 

1085 proc = self.repo.git.fetch( 

1086 "--", self, *args, as_process=True, with_stdout=False, universal_newlines=True, v=bool(verbose), **kwargs 

1087 ) 

1088 res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout) 

1089 if hasattr(self.repo.odb, "update_cache"): 

1090 self.repo.odb.update_cache() 

1091 return res 

1092 

1093 def pull( 

1094 self, 

1095 refspec: Union[str, List[str], None] = None, 

1096 progress: Progress = None, 

1097 kill_after_timeout: Union[None, float] = None, 

1098 allow_unsafe_protocols: bool = False, 

1099 allow_unsafe_options: bool = False, 

1100 **kwargs: Any, 

1101 ) -> IterableList[FetchInfo]: 

1102 """Pull changes from the given branch, being the same as a fetch followed by a 

1103 merge of branch with your local branch. 

1104 

1105 :param refspec: 

1106 See :meth:`fetch` method. Values starting with ``-`` are rejected, 

1107 even when ``allow_unsafe_options`` is enabled. Pass options as keywords. 

1108 

1109 :param progress: 

1110 See :meth:`push` method. 

1111 

1112 :param kill_after_timeout: 

1113 See :meth:`fetch` method. 

1114 

1115 :param allow_unsafe_protocols: 

1116 Allow unsafe protocols to be used, like ``ext``. 

1117 

1118 :param allow_unsafe_options: 

1119 Allow unsafe options to be used, like ``--upload-pack``. 

1120 

1121 :param kwargs: 

1122 Additional arguments to be passed to :manpage:`git-pull(1)`. 

1123 

1124 :return: 

1125 Please see :meth:`fetch` method. 

1126 """ 

1127 if refspec is None: 

1128 # No argument refspec, then ensure the repo's config has a fetch refspec. 

1129 self._assert_refspec() 

1130 kwargs = add_progress(kwargs, self.repo.git, progress) 

1131 

1132 refspec = Git._unpack_args(refspec or []) 

1133 # Git pull forwards these operands to fetch without preserving `--`. 

1134 # Reject every option-shaped operand, including with unsafe options enabled: 

1135 # opting into an explicit option must not turn a refspec into an option. 

1136 for operand in [self.name, *refspec]: 

1137 if operand.startswith("-"): 

1138 raise UnsafeOptionError("Remote names and pull refspecs must not start with '-'.") 

1139 if not allow_unsafe_protocols: 

1140 self.repo.git._check_unsafe_protocols_in_args([self, *refspec], kwargs) 

1141 

1142 if not allow_unsafe_options: 

1143 Git.check_unsafe_options( 

1144 options=Git._option_candidates([], kwargs), 

1145 unsafe_options=self.unsafe_git_pull_options, 

1146 ) 

1147 

1148 proc = self.repo.git.pull( 

1149 "--", self, refspec, with_stdout=False, as_process=True, universal_newlines=True, v=True, **kwargs 

1150 ) 

1151 res = self._get_fetch_info_from_stderr(proc, progress, kill_after_timeout=kill_after_timeout) 

1152 if hasattr(self.repo.odb, "update_cache"): 

1153 self.repo.odb.update_cache() 

1154 return res 

1155 

1156 def push( 

1157 self, 

1158 refspec: Union[str, List[str], None] = None, 

1159 progress: Progress = None, 

1160 kill_after_timeout: Union[None, float] = None, 

1161 allow_unsafe_protocols: bool = False, 

1162 allow_unsafe_options: bool = False, 

1163 **kwargs: Any, 

1164 ) -> PushInfoList: 

1165 """Push changes from source branch in refspec to target branch in refspec. 

1166 

1167 :param refspec: 

1168 See :meth:`fetch` method. 

1169 

1170 :param progress: 

1171 Can take one of many value types: 

1172 

1173 * ``None``, to discard progress information. 

1174 * A function (callable) that is called with the progress information. 

1175 Signature: ``progress(op_code, cur_count, max_count=None, message='')``. 

1176 See :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` for a 

1177 description of all arguments given to the function. 

1178 * An instance of a class derived from :class:`~git.util.RemoteProgress` that 

1179 overrides the 

1180 :meth:`RemoteProgress.update <git.util.RemoteProgress.update>` method. 

1181 

1182 :note: 

1183 No further progress information is returned after push returns. 

1184 

1185 :param kill_after_timeout: 

1186 To specify a timeout in seconds for the git command, after which the process 

1187 should be killed. It is set to ``None`` by default. 

1188 

1189 :param allow_unsafe_protocols: 

1190 Allow unsafe protocols to be used, like ``ext``. 

1191 

1192 :param allow_unsafe_options: 

1193 Allow unsafe options to be used, like ``--receive-pack``. 

1194 

1195 :param kwargs: 

1196 Additional arguments to be passed to :manpage:`git-push(1)`. 

1197 

1198 :return: 

1199 A :class:`PushInfoList` object, where each list member represents an 

1200 individual head which had been updated on the remote side. 

1201 

1202 If the push contains rejected heads, these will have the 

1203 :const:`PushInfo.ERROR` bit set in their flags. 

1204 

1205 If the operation fails completely, the length of the returned 

1206 :class:`PushInfoList` will be 0. 

1207 

1208 Call :meth:`~PushInfoList.raise_if_error` on the returned object to raise on 

1209 any failure. 

1210 """ 

1211 kwargs = add_progress(kwargs, self.repo.git, progress) 

1212 

1213 refspec = Git._unpack_args(refspec or []) 

1214 if not allow_unsafe_protocols: 

1215 self.repo.git._check_unsafe_protocols_in_args([self, *refspec], kwargs) 

1216 

1217 if not allow_unsafe_options: 

1218 Git.check_unsafe_options( 

1219 options=Git._option_candidates([], kwargs), 

1220 unsafe_options=self.unsafe_git_push_options, 

1221 ) 

1222 

1223 proc = self.repo.git.push( 

1224 "--", 

1225 self, 

1226 refspec, 

1227 porcelain=True, 

1228 as_process=True, 

1229 universal_newlines=True, 

1230 kill_after_timeout=kill_after_timeout, 

1231 **kwargs, 

1232 ) 

1233 return self._get_push_info(proc, progress, kill_after_timeout=kill_after_timeout) 

1234 

1235 @property 

1236 def config_reader(self) -> SectionConstraint[GitConfigParser]: 

1237 """ 

1238 :return: 

1239 :class:`~git.config.GitConfigParser` compatible object able to read options 

1240 for only our remote. Hence you may simply type ``config.get("pushurl")`` to 

1241 obtain the information. 

1242 """ 

1243 return self._config_reader 

1244 

1245 def _clear_cache(self) -> None: 

1246 try: 

1247 del self._config_reader 

1248 except AttributeError: 

1249 pass 

1250 # END handle exception 

1251 

1252 @property 

1253 def config_writer(self) -> SectionConstraint: 

1254 """ 

1255 :return: 

1256 :class:`~git.config.GitConfigParser`-compatible object able to write options 

1257 for this remote. 

1258 

1259 :note: 

1260 You can only own one writer at a time - delete it to release the 

1261 configuration file and make it usable by others. 

1262 

1263 To assure consistent results, you should only query options through the 

1264 writer. Once you are done writing, you are free to use the config reader 

1265 once again. 

1266 """ 

1267 writer = self.repo.config_writer() 

1268 

1269 # Clear our cache to ensure we re-read the possibly changed configuration. 

1270 self._clear_cache() 

1271 return SectionConstraint(writer, self._config_section_name())