Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/objects/submodule/root.py: 20%

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

143 statements  

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

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

3 

4__all__ = ["RootModule", "RootUpdateProgress"] 

5 

6import logging 

7 

8# typing ------------------------------------------------------------------- 

9from typing import TYPE_CHECKING, Union 

10 

11import git 

12from git.exc import InvalidGitRepositoryError 

13from git.types import Commit_ish 

14from git.util import IterableList 

15 

16from .base import Submodule, UpdateProgress 

17from .util import find_first_remote_branch 

18 

19if TYPE_CHECKING: 

20 from git.repo import Repo 

21 

22# ---------------------------------------------------------------------------- 

23 

24_logger = logging.getLogger(__name__) 

25 

26 

27class RootUpdateProgress(UpdateProgress): 

28 """Utility class which adds more opcodes to 

29 :class:`~git.objects.submodule.base.UpdateProgress`.""" 

30 

31 REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [ 

32 1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4) 

33 ] 

34 _num_op_codes = UpdateProgress._num_op_codes + 4 

35 

36 __slots__ = () 

37 

38 

39BEGIN = RootUpdateProgress.BEGIN 

40END = RootUpdateProgress.END 

41REMOVE = RootUpdateProgress.REMOVE 

42BRANCHCHANGE = RootUpdateProgress.BRANCHCHANGE 

43URLCHANGE = RootUpdateProgress.URLCHANGE 

44PATHCHANGE = RootUpdateProgress.PATHCHANGE 

45 

46 

47class RootModule(Submodule): 

48 """A (virtual) root of all submodules in the given repository. 

49 

50 This can be used to more easily traverse all submodules of the 

51 superproject (master repository). 

52 """ 

53 

54 __slots__ = () 

55 

56 k_root_name = "__ROOT__" 

57 

58 def __init__(self, repo: "Repo") -> None: 

59 # repo, binsha, mode=None, path=None, name = None, parent_commit=None, url=None, ref=None) 

60 super().__init__( 

61 repo, 

62 binsha=self.NULL_BIN_SHA, 

63 mode=self.k_default_mode, 

64 path="", 

65 name=self.k_root_name, 

66 parent_commit=repo.head.commit, 

67 url="", 

68 branch_path=git.Head.to_full_path(self.k_head_default), 

69 ) 

70 

71 def _clear_cache(self) -> None: 

72 """May not do anything.""" 

73 pass 

74 

75 # { Interface 

76 

77 def update( # type: ignore[override] 

78 self, 

79 previous_commit: Union[Commit_ish, str, None] = None, 

80 recursive: bool = True, 

81 force_remove: bool = False, 

82 init: bool = True, 

83 to_latest_revision: bool = False, 

84 progress: Union[None, "RootUpdateProgress"] = None, 

85 dry_run: bool = False, 

86 force_reset: bool = False, 

87 keep_going: bool = False, 

88 no_fetch: bool = False, 

89 ) -> "RootModule": 

90 """Update the submodules of this repository to the current HEAD commit. 

91 

92 This method behaves smartly by determining changes of the path of a submodule's 

93 repository, next to changes to the to-be-checked-out commit or the branch to be 

94 checked out. This works if the submodule's ID does not change. 

95 

96 Additionally it will detect addition and removal of submodules, which will be 

97 handled gracefully. 

98 

99 :param previous_commit: 

100 If set to a commit-ish, the commit we should use as the previous commit the 

101 HEAD pointed to before it was set to the commit it points to now. 

102 If ``None``, it defaults to ``HEAD@{1}`` otherwise. 

103 

104 :param recursive: 

105 If ``True``, the children of submodules will be updated as well using the 

106 same technique. 

107 

108 :param force_remove: 

109 If submodules have been deleted, they will be forcibly removed. Otherwise 

110 the update may fail if a submodule's repository cannot be deleted as changes 

111 have been made to it. 

112 (See :meth:`Submodule.update <git.objects.submodule.base.Submodule.update>` 

113 for more information.) 

114 

115 :param init: 

116 If we encounter a new module which would need to be initialized, then do it. 

117 

118 :param to_latest_revision: 

119 If ``True``, instead of checking out the revision pointed to by this 

120 submodule's sha, the checked out tracking branch will be merged with the 

121 latest remote branch fetched from the repository's origin. 

122 

123 Unless `force_reset` is specified, a local tracking branch will never be 

124 reset into its past, therefore the remote branch must be in the future for 

125 this to have an effect. 

126 

127 :param force_reset: 

128 If ``True``, submodules may checkout or reset their branch even if the 

129 repository has pending changes that would be overwritten, or if the local 

130 tracking branch is in the future of the remote tracking branch and would be 

131 reset into its past. 

132 

133 :param progress: 

134 :class:`RootUpdateProgress` instance, or ``None`` if no progress should be 

135 sent. 

136 

137 :param dry_run: 

138 If ``True``, operations will not actually be performed. Progress messages 

139 will change accordingly to indicate the WOULD DO state of the operation. 

140 

141 :param keep_going: 

142 If ``True``, we will ignore but log all errors, and keep going recursively. 

143 Unless `dry_run` is set as well, `keep_going` could cause 

144 subsequent/inherited errors you wouldn't see otherwise. 

145 In conjunction with `dry_run`, this can be useful to anticipate all errors 

146 when updating submodules. 

147 

148 :param no_fetch: 

149 If ``True``, update using locally available objects and remote-tracking 

150 refs without fetching or cloning. Cached refs are preserved and used even 

151 when a submodule's URL changes. 

152 

153 :return: 

154 self 

155 """ 

156 if self.repo.bare: 

157 raise InvalidGitRepositoryError("Cannot update submodules in bare repositories") 

158 # END handle bare 

159 

160 if progress is None: 

161 progress = RootUpdateProgress() 

162 # END ensure progress is set 

163 

164 prefix = "" 

165 if dry_run: 

166 prefix = "DRY-RUN: " 

167 

168 repo = self.repo 

169 sms: "IterableList[Submodule]" = IterableList("name") 

170 

171 try: 

172 # SETUP BASE COMMIT 

173 ################### 

174 cur_commit = repo.head.commit 

175 if previous_commit is None: 

176 try: 

177 previous_commit = repo.commit(repo.head.log_entry(-1).oldhexsha) 

178 if previous_commit.binsha == previous_commit.NULL_BIN_SHA: 

179 raise IndexError 

180 # END handle initial commit 

181 except IndexError: 

182 # In new repositories, there is no previous commit. 

183 previous_commit = cur_commit 

184 # END exception handling 

185 else: 

186 previous_commit = repo.commit(previous_commit) # Obtain commit object. 

187 # END handle previous commit 

188 

189 psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit) 

190 sms = self.list_items(repo) 

191 spsms = set(psms) 

192 ssms = set(sms) 

193 

194 # HANDLE REMOVALS 

195 ################### 

196 rrsm = spsms - ssms 

197 len_rrsm = len(rrsm) 

198 

199 for i, rsm in enumerate(rrsm): 

200 op = REMOVE 

201 if i == 0: 

202 op |= BEGIN 

203 # END handle begin 

204 

205 # Fake it into thinking its at the current commit to allow deletion 

206 # of previous module. Trigger the cache to be updated before that. 

207 progress.update( 

208 op, 

209 i, 

210 len_rrsm, 

211 prefix + "Removing submodule %r at %s" % (rsm.name, rsm.abspath), 

212 ) 

213 rsm._parent_commit = repo.head.commit 

214 rsm.remove( 

215 configuration=False, 

216 module=True, 

217 force=force_remove, 

218 dry_run=dry_run, 

219 ) 

220 

221 if i == len_rrsm - 1: 

222 op |= END 

223 # END handle end 

224 progress.update(op, i, len_rrsm, prefix + "Done removing submodule %r" % rsm.name) 

225 # END for each removed submodule 

226 

227 # HANDLE PATH RENAMES 

228 ##################### 

229 # URL changes + branch changes. 

230 csms = spsms & ssms 

231 len_csms = len(csms) 

232 for i, csm in enumerate(csms): 

233 psm: "Submodule" = psms[csm.name] 

234 sm: "Submodule" = sms[csm.name] 

235 

236 # PATH CHANGES 

237 ############## 

238 if sm.path != psm.path and psm.module_exists(): 

239 progress.update( 

240 BEGIN | PATHCHANGE, 

241 i, 

242 len_csms, 

243 prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath), 

244 ) 

245 # Move the module to the new path. 

246 if not dry_run: 

247 psm.move(sm.path, module=True, configuration=False) 

248 # END handle dry_run 

249 progress.update( 

250 END | PATHCHANGE, 

251 i, 

252 len_csms, 

253 prefix + "Done moving repository of submodule %r" % sm.name, 

254 ) 

255 # END handle path changes 

256 

257 if sm.module_exists(): 

258 # HANDLE URL CHANGE 

259 ################### 

260 if sm.url != psm.url: 

261 # When fetching, add the new remote and remove the old one. 

262 # This way, if the url just changes, the commits will not have 

263 # to be re-retrieved. 

264 nn = "__new_origin__" 

265 smm = sm.module() 

266 rmts = smm.remotes 

267 # Relative urls are resolved against the superproject's remote, 

268 # which is what the module's remotes hold. 

269 sm_url = sm._resolved_url() 

270 

271 # Don't do anything if we already have the url we search in 

272 # place. 

273 if len([r for r in rmts if r.url == sm_url]) == 0: 

274 progress.update( 

275 BEGIN | URLCHANGE, 

276 i, 

277 len_csms, 

278 prefix + "Changing url of submodule %r from %s to %s" % (sm.name, psm.url, sm.url), 

279 ) 

280 

281 if not dry_run: 

282 previous_remote = None 

283 if len(rmts) == 1: 

284 previous_remote = rmts[0] 

285 else: 

286 psm_url = psm._resolved_url() 

287 for remote in rmts: 

288 if remote.url == psm_url: 

289 previous_remote = remote 

290 break 

291 # END if urls match 

292 # END for each remote 

293 

294 if previous_remote is None: 

295 raise InvalidGitRepositoryError( 

296 "Couldn't find original remote-repo at url %r" % psm.url 

297 ) 

298 # END handle check we found a remote 

299 

300 if no_fetch: 

301 # A new remote would have no cached refs. Preserve 

302 # the existing refs and tracking configuration for 

303 # offline updates instead of replacing the remote. 

304 previous_remote.set_url(git.Git.polish_url(sm_url, expand_vars=False)) 

305 else: 

306 assert nn not in [r.name for r in rmts] 

307 smr = smm.create_remote(nn, sm_url) 

308 smr.fetch(progress=progress) 

309 

310 # If we have a tracking branch, it should be available 

311 # in the new remote as well. 

312 if len([r for r in smr.refs if r.remote_head == sm.branch_name]) == 0: 

313 raise ValueError( 

314 "Submodule branch named %r was not available in new submodule remote at %r" 

315 % (sm.branch_name, sm.url) 

316 ) 

317 # END head is not detached 

318 

319 orig_name = previous_remote.name 

320 smm.delete_remote(previous_remote) 

321 # NOTE: Currently we leave tags from the deleted remotes 

322 # as well as separate tracking branches in the possibly 

323 # totally changed repository (someone could have changed 

324 # the url to another project). At some point, one might 

325 # want to clean it up, but the danger is high to remove 

326 # stuff the user has added explicitly. 

327 

328 # Rename the new remote back to what it was. 

329 smr.rename(orig_name) 

330 

331 # Early on, we verified that the our current tracking 

332 # branch exists in the remote. Now we have to ensure 

333 # that the sha we point to is still contained in the new 

334 # remote tracking branch. 

335 smsha = sm.binsha 

336 found = False 

337 rref = smr.refs[self.branch_name] 

338 for c in rref.commit.traverse(): 

339 if c.binsha == smsha: 

340 found = True 

341 break 

342 # END traverse all commits in search for sha 

343 # END for each commit 

344 

345 if not found: 

346 # Adjust our internal binsha to use the one of the 

347 # remote this way, it will be checked out in the 

348 # next step. This will change the submodule relative 

349 # to us, so the user will be able to commit the 

350 # change easily. 

351 _logger.warning( 

352 "Current sha %s was not contained in the tracking\ 

353 branch at the new remote, setting it the the remote's tracking branch", 

354 sm.hexsha, 

355 ) 

356 sm.binsha = rref.commit.binsha 

357 # END reset binsha 

358 

359 # NOTE: All checkout is performed by the base 

360 # implementation of update. 

361 # END handle dry_run 

362 progress.update( 

363 END | URLCHANGE, 

364 i, 

365 len_csms, 

366 prefix + "Done adjusting url of submodule %r" % (sm.name), 

367 ) 

368 # END skip remote handling if new url already exists in module 

369 # END handle url 

370 

371 # HANDLE PATH CHANGES 

372 ##################### 

373 if sm.branch_path != psm.branch_path: 

374 # Finally, create a new tracking branch which tracks the new 

375 # remote branch. 

376 progress.update( 

377 BEGIN | BRANCHCHANGE, 

378 i, 

379 len_csms, 

380 prefix 

381 + "Changing branch of submodule %r from %s to %s" 

382 % (sm.name, psm.branch_path, sm.branch_path), 

383 ) 

384 if not dry_run: 

385 smm = sm.module() 

386 smmr = smm.remotes 

387 # As the branch might not exist yet, fetch all remotes 

388 # unless restricted to locally cached refs. 

389 if not no_fetch: 

390 for remote in smmr: 

391 remote.fetch(progress=progress) 

392 # END for each remote 

393 

394 try: 

395 tbr = git.Head.create( 

396 smm, 

397 sm.branch_name, 

398 logmsg="branch: Created from HEAD", 

399 ) 

400 except OSError: 

401 # ...or reuse the existing one. 

402 tbr = git.Head(smm, sm.branch_path) 

403 # END ensure tracking branch exists 

404 

405 tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name)) 

406 # NOTE: All head-resetting is done in the base 

407 # implementation of update but we will have to checkout the 

408 # new branch here. As it still points to the currently 

409 # checked out commit, we don't do any harm. 

410 # As we don't want to update working-tree or index, changing 

411 # the ref is all there is to do. 

412 smm.head.reference = tbr 

413 # END handle dry_run 

414 

415 progress.update( 

416 END | BRANCHCHANGE, 

417 i, 

418 len_csms, 

419 prefix + "Done changing branch of submodule %r" % sm.name, 

420 ) 

421 # END handle branch 

422 # END handle 

423 # END for each common submodule 

424 except Exception as err: 

425 if not keep_going: 

426 raise 

427 _logger.error(str(err)) 

428 # END handle keep_going 

429 

430 # FINALLY UPDATE ALL ACTUAL SUBMODULES 

431 ###################################### 

432 for sm in sms: 

433 # Update the submodule using the default method. 

434 sm.update( 

435 recursive=False, 

436 init=init, 

437 to_latest_revision=to_latest_revision, 

438 progress=progress, 

439 dry_run=dry_run, 

440 force=force_reset, 

441 keep_going=keep_going, 

442 no_fetch=no_fetch, 

443 ) 

444 

445 # Update recursively depth first - question is which inconsistent state will 

446 # be better in case it fails somewhere. Defective branch or defective depth. 

447 # The RootSubmodule type will never process itself, which was done in the 

448 # previous expression. 

449 if recursive: 

450 # The module would exist by now if we are not in dry_run mode. 

451 if sm.module_exists(): 

452 type(self)(sm.module()).update( 

453 recursive=True, 

454 force_remove=force_remove, 

455 init=init, 

456 to_latest_revision=to_latest_revision, 

457 progress=progress, 

458 dry_run=dry_run, 

459 force_reset=force_reset, 

460 keep_going=keep_going, 

461 no_fetch=no_fetch, 

462 ) 

463 # END handle dry_run 

464 # END handle recursive 

465 # END for each submodule to update 

466 

467 return self 

468 

469 def module(self) -> "Repo": 

470 """:return: The actual repository containing the submodules""" 

471 return self.repo 

472 

473 # } END interface 

474 

475 

476# } END classes