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

138 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 

8import git 

9from git.exc import InvalidGitRepositoryError 

10from git.util import IterableList 

11 

12from .base import Submodule, UpdateProgress 

13from .util import find_first_remote_branch 

14 

15# typing ------------------------------------------------------------------- 

16 

17from typing import TYPE_CHECKING, Union 

18 

19from git.types import Commit_ish 

20 

21if TYPE_CHECKING: 

22 from git.repo import Repo 

23 

24# ---------------------------------------------------------------------------- 

25 

26_logger = logging.getLogger(__name__) 

27 

28 

29class RootUpdateProgress(UpdateProgress): 

30 """Utility class which adds more opcodes to 

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

32 

33 REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [ 

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

35 ] 

36 _num_op_codes = UpdateProgress._num_op_codes + 4 

37 

38 __slots__ = () 

39 

40 

41BEGIN = RootUpdateProgress.BEGIN 

42END = RootUpdateProgress.END 

43REMOVE = RootUpdateProgress.REMOVE 

44BRANCHCHANGE = RootUpdateProgress.BRANCHCHANGE 

45URLCHANGE = RootUpdateProgress.URLCHANGE 

46PATHCHANGE = RootUpdateProgress.PATHCHANGE 

47 

48 

49class RootModule(Submodule): 

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

51 

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

53 superproject (master repository). 

54 """ 

55 

56 __slots__ = () 

57 

58 k_root_name = "__ROOT__" 

59 

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

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

62 super().__init__( 

63 repo, 

64 binsha=self.NULL_BIN_SHA, 

65 mode=self.k_default_mode, 

66 path="", 

67 name=self.k_root_name, 

68 parent_commit=repo.head.commit, 

69 url="", 

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

71 ) 

72 

73 def _clear_cache(self) -> None: 

74 """May not do anything.""" 

75 pass 

76 

77 # { Interface 

78 

79 def update( # type: ignore[override] 

80 self, 

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

82 recursive: bool = True, 

83 force_remove: bool = False, 

84 init: bool = True, 

85 to_latest_revision: bool = False, 

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

87 dry_run: bool = False, 

88 force_reset: bool = False, 

89 keep_going: bool = False, 

90 ) -> "RootModule": 

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

92 

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

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

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

96 

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

98 handled gracefully. 

99 

100 :param previous_commit: 

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

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

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

104 

105 :param recursive: 

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

107 same technique. 

108 

109 :param force_remove: 

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

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

112 have been made to it. 

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

114 for more information.) 

115 

116 :param init: 

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

118 

119 :param to_latest_revision: 

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

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

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

123 

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

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

126 this to have an effect. 

127 

128 :param force_reset: 

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

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

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

132 reset into its past. 

133 

134 :param progress: 

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

136 sent. 

137 

138 :param dry_run: 

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

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

141 

142 :param keep_going: 

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

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

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

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

147 when updating submodules. 

148 

149 :return: 

150 self 

151 """ 

152 if self.repo.bare: 

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

154 # END handle bare 

155 

156 if progress is None: 

157 progress = RootUpdateProgress() 

158 # END ensure progress is set 

159 

160 prefix = "" 

161 if dry_run: 

162 prefix = "DRY-RUN: " 

163 

164 repo = self.repo 

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

166 

167 try: 

168 # SETUP BASE COMMIT 

169 ################### 

170 cur_commit = repo.head.commit 

171 if previous_commit is None: 

172 try: 

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

174 if previous_commit.binsha == previous_commit.NULL_BIN_SHA: 

175 raise IndexError 

176 # END handle initial commit 

177 except IndexError: 

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

179 previous_commit = cur_commit 

180 # END exception handling 

181 else: 

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

183 # END handle previous commit 

184 

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

186 sms = self.list_items(repo) 

187 spsms = set(psms) 

188 ssms = set(sms) 

189 

190 # HANDLE REMOVALS 

191 ################### 

192 rrsm = spsms - ssms 

193 len_rrsm = len(rrsm) 

194 

195 for i, rsm in enumerate(rrsm): 

196 op = REMOVE 

197 if i == 0: 

198 op |= BEGIN 

199 # END handle begin 

200 

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

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

203 progress.update( 

204 op, 

205 i, 

206 len_rrsm, 

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

208 ) 

209 rsm._parent_commit = repo.head.commit 

210 rsm.remove( 

211 configuration=False, 

212 module=True, 

213 force=force_remove, 

214 dry_run=dry_run, 

215 ) 

216 

217 if i == len_rrsm - 1: 

218 op |= END 

219 # END handle end 

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

221 # END for each removed submodule 

222 

223 # HANDLE PATH RENAMES 

224 ##################### 

225 # URL changes + branch changes. 

226 csms = spsms & ssms 

227 len_csms = len(csms) 

228 for i, csm in enumerate(csms): 

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

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

231 

232 # PATH CHANGES 

233 ############## 

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

235 progress.update( 

236 BEGIN | PATHCHANGE, 

237 i, 

238 len_csms, 

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

240 ) 

241 # Move the module to the new path. 

242 if not dry_run: 

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

244 # END handle dry_run 

245 progress.update( 

246 END | PATHCHANGE, 

247 i, 

248 len_csms, 

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

250 ) 

251 # END handle path changes 

252 

253 if sm.module_exists(): 

254 # HANDLE URL CHANGE 

255 ################### 

256 if sm.url != psm.url: 

257 # Add the new remote, remove the old one. 

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

259 # to be re-retrieved. 

260 nn = "__new_origin__" 

261 smm = sm.module() 

262 rmts = smm.remotes 

263 

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

265 # place. 

266 if len([r for r in rmts if r.url == sm.url]) == 0: 

267 progress.update( 

268 BEGIN | URLCHANGE, 

269 i, 

270 len_csms, 

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

272 ) 

273 

274 if not dry_run: 

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

276 smr = smm.create_remote(nn, sm.url) 

277 smr.fetch(progress=progress) 

278 

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

280 # in the new remote as well. 

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

282 raise ValueError( 

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

284 % (sm.branch_name, sm.url) 

285 ) 

286 # END head is not detached 

287 

288 # Now delete the changed one. 

289 rmt_for_deletion = None 

290 for remote in rmts: 

291 if remote.url == psm.url: 

292 rmt_for_deletion = remote 

293 break 

294 # END if urls match 

295 # END for each remote 

296 

297 # If we didn't find a matching remote, but have exactly 

298 # one, we can safely use this one. 

299 if rmt_for_deletion is None: 

300 if len(rmts) == 1: 

301 rmt_for_deletion = rmts[0] 

302 else: 

303 # If we have not found any remote with the 

304 # original URL we may not have a name. This is a 

305 # special case, and its okay to fail here. 

306 # Alternatively we could just generate a unique 

307 # name and leave all existing ones in place. 

308 raise InvalidGitRepositoryError( 

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

310 ) 

311 # END handle one single remote 

312 # END handle check we found a remote 

313 

314 orig_name = rmt_for_deletion.name 

315 smm.delete_remote(rmt_for_deletion) 

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

317 # as well as separate tracking branches in the possibly 

318 # totally changed repository (someone could have changed 

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

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

321 # stuff the user has added explicitly. 

322 

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

324 smr.rename(orig_name) 

325 

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

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

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

329 # remote tracking branch. 

330 smsha = sm.binsha 

331 found = False 

332 rref = smr.refs[self.branch_name] 

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

334 if c.binsha == smsha: 

335 found = True 

336 break 

337 # END traverse all commits in search for sha 

338 # END for each commit 

339 

340 if not found: 

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

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

343 # next step. This will change the submodule relative 

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

345 # change easily. 

346 _logger.warning( 

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

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

349 sm.hexsha, 

350 ) 

351 sm.binsha = rref.commit.binsha 

352 # END reset binsha 

353 

354 # NOTE: All checkout is performed by the base 

355 # implementation of update. 

356 # END handle dry_run 

357 progress.update( 

358 END | URLCHANGE, 

359 i, 

360 len_csms, 

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

362 ) 

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

364 # END handle url 

365 

366 # HANDLE PATH CHANGES 

367 ##################### 

368 if sm.branch_path != psm.branch_path: 

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

370 # remote branch. 

371 progress.update( 

372 BEGIN | BRANCHCHANGE, 

373 i, 

374 len_csms, 

375 prefix 

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

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

378 ) 

379 if not dry_run: 

380 smm = sm.module() 

381 smmr = smm.remotes 

382 # As the branch might not exist yet, we will have to fetch 

383 # all remotes to be sure... 

384 for remote in smmr: 

385 remote.fetch(progress=progress) 

386 # END for each remote 

387 

388 try: 

389 tbr = git.Head.create( 

390 smm, 

391 sm.branch_name, 

392 logmsg="branch: Created from HEAD", 

393 ) 

394 except OSError: 

395 # ...or reuse the existing one. 

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

397 # END ensure tracking branch exists 

398 

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

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

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

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

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

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

405 # the ref is all there is to do. 

406 smm.head.reference = tbr 

407 # END handle dry_run 

408 

409 progress.update( 

410 END | BRANCHCHANGE, 

411 i, 

412 len_csms, 

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

414 ) 

415 # END handle branch 

416 # END handle 

417 # END for each common submodule 

418 except Exception as err: 

419 if not keep_going: 

420 raise 

421 _logger.error(str(err)) 

422 # END handle keep_going 

423 

424 # FINALLY UPDATE ALL ACTUAL SUBMODULES 

425 ###################################### 

426 for sm in sms: 

427 # Update the submodule using the default method. 

428 sm.update( 

429 recursive=False, 

430 init=init, 

431 to_latest_revision=to_latest_revision, 

432 progress=progress, 

433 dry_run=dry_run, 

434 force=force_reset, 

435 keep_going=keep_going, 

436 ) 

437 

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

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

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

441 # previous expression. 

442 if recursive: 

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

444 if sm.module_exists(): 

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

446 recursive=True, 

447 force_remove=force_remove, 

448 init=init, 

449 to_latest_revision=to_latest_revision, 

450 progress=progress, 

451 dry_run=dry_run, 

452 force_reset=force_reset, 

453 keep_going=keep_going, 

454 ) 

455 # END handle dry_run 

456 # END handle recursive 

457 # END for each submodule to update 

458 

459 return self 

460 

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

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

463 return self.repo 

464 

465 # } END interface 

466 

467 

468# } END classes