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

467 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: 

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

993 output.error = e 

994 

995 return output 

996 

997 def _assert_refspec(self) -> None: 

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

999 config = self.config_reader 

1000 unset = "placeholder" 

1001 try: 

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

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

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

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

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

1007 finally: 

1008 config.release() 

1009 

1010 def fetch( 

1011 self, 

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

1013 progress: Progress = None, 

1014 verbose: bool = True, 

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

1016 allow_unsafe_protocols: bool = False, 

1017 allow_unsafe_options: bool = False, 

1018 **kwargs: Any, 

1019 ) -> IterableList[FetchInfo]: 

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

1021 

1022 :param refspec: 

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

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

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

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

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

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

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

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

1031 

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

1033 

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

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

1036 this facility. 

1037 

1038 :param progress: 

1039 See the :meth:`push` method. 

1040 

1041 :param verbose: 

1042 Boolean for verbose output. 

1043 

1044 :param kill_after_timeout: 

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

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

1047 

1048 :param allow_unsafe_protocols: 

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

1050 

1051 :param allow_unsafe_options: 

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

1053 

1054 :param kwargs: 

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

1056 

1057 :return: 

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

1059 detailed information about the fetch results 

1060 

1061 :note: 

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

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

1064 """ 

1065 if refspec is None: 

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

1067 self._assert_refspec() 

1068 

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

1070 if isinstance(refspec, list): 

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

1072 else: 

1073 args = [refspec] 

1074 

1075 if not allow_unsafe_protocols: 

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

1077 

1078 if not allow_unsafe_options: 

1079 Git.check_unsafe_options( 

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

1081 unsafe_options=self.unsafe_git_fetch_options, 

1082 ) 

1083 

1084 proc = self.repo.git.fetch( 

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

1086 ) 

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

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

1089 self.repo.odb.update_cache() 

1090 return res 

1091 

1092 def pull( 

1093 self, 

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

1095 progress: Progress = None, 

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

1097 allow_unsafe_protocols: bool = False, 

1098 allow_unsafe_options: bool = False, 

1099 **kwargs: Any, 

1100 ) -> IterableList[FetchInfo]: 

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

1102 merge of branch with your local branch. 

1103 

1104 :param refspec: 

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

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

1107 

1108 :param progress: 

1109 See :meth:`push` method. 

1110 

1111 :param kill_after_timeout: 

1112 See :meth:`fetch` method. 

1113 

1114 :param allow_unsafe_protocols: 

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

1116 

1117 :param allow_unsafe_options: 

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

1119 

1120 :param kwargs: 

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

1122 

1123 :return: 

1124 Please see :meth:`fetch` method. 

1125 """ 

1126 if refspec is None: 

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

1128 self._assert_refspec() 

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

1130 

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

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

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

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

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

1136 if operand.startswith("-"): 

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

1138 if not allow_unsafe_protocols: 

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

1140 

1141 if not allow_unsafe_options: 

1142 Git.check_unsafe_options( 

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

1144 unsafe_options=self.unsafe_git_pull_options, 

1145 ) 

1146 

1147 proc = self.repo.git.pull( 

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

1149 ) 

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

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

1152 self.repo.odb.update_cache() 

1153 return res 

1154 

1155 def push( 

1156 self, 

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

1158 progress: Progress = None, 

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

1160 allow_unsafe_protocols: bool = False, 

1161 allow_unsafe_options: bool = False, 

1162 **kwargs: Any, 

1163 ) -> PushInfoList: 

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

1165 

1166 :param refspec: 

1167 See :meth:`fetch` method. 

1168 

1169 :param progress: 

1170 Can take one of many value types: 

1171 

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

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

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

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

1176 description of all arguments given to the function. 

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

1178 overrides the 

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

1180 

1181 :note: 

1182 No further progress information is returned after push returns. 

1183 

1184 :param kill_after_timeout: 

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

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

1187 

1188 :param allow_unsafe_protocols: 

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

1190 

1191 :param allow_unsafe_options: 

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

1193 

1194 :param kwargs: 

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

1196 

1197 :return: 

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

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

1200 

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

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

1203 

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

1205 :class:`PushInfoList` will be 0. 

1206 

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

1208 any failure. 

1209 """ 

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

1211 

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

1213 if not allow_unsafe_protocols: 

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

1215 

1216 if not allow_unsafe_options: 

1217 Git.check_unsafe_options( 

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

1219 unsafe_options=self.unsafe_git_push_options, 

1220 ) 

1221 

1222 proc = self.repo.git.push( 

1223 "--", 

1224 self, 

1225 refspec, 

1226 porcelain=True, 

1227 as_process=True, 

1228 universal_newlines=True, 

1229 kill_after_timeout=kill_after_timeout, 

1230 **kwargs, 

1231 ) 

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

1233 

1234 @property 

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

1236 """ 

1237 :return: 

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

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

1240 obtain the information. 

1241 """ 

1242 return self._config_reader 

1243 

1244 def _clear_cache(self) -> None: 

1245 try: 

1246 del self._config_reader 

1247 except AttributeError: 

1248 pass 

1249 # END handle exception 

1250 

1251 @property 

1252 def config_writer(self) -> SectionConstraint: 

1253 """ 

1254 :return: 

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

1256 for this remote. 

1257 

1258 :note: 

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

1260 configuration file and make it usable by others. 

1261 

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

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

1264 once again. 

1265 """ 

1266 writer = self.repo.config_writer() 

1267 

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

1269 self._clear_cache() 

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