Coverage for pyVersioning/__init__.py: 71%

462 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-17 22:22 +0000

1# ==================================================================================================================== # 

2# __ __ _ _ # 

3# _ __ _ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ # 

4# | '_ \| | | \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | # 

5# | |_) | |_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | # 

6# | .__/ \__, | \_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2020-2025 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31__author__ = "Patrick Lehmann" 

32__email__ = "Paebbels@gmail.com" 

33__copyright__ = "2020-2025, Patrick Lehmann" 

34__license__ = "Apache License, Version 2.0" 

35__version__ = "0.18.3" 

36__keywords__ = ["Python3", "Template", "Versioning", "Git"] 

37 

38from dataclasses import make_dataclass 

39from datetime import date, time, datetime 

40from enum import Enum, auto 

41from os import environ 

42from subprocess import run as subprocess_run, PIPE, CalledProcessError 

43from sys import version_info 

44from typing import Union, Any, Dict, Tuple, ClassVar, Generator, Optional as Nullable, List 

45 

46from pyTooling.Decorators import export, readonly 

47from pyTooling.MetaClasses import ExtendedType 

48from pyTooling.Versioning import SemanticVersion 

49from pyTooling.TerminalUI import ILineTerminal 

50 

51from pyVersioning.Configuration import Configuration, Project, Compiler, Build 

52 

53 

54@export 

55class VersioningException(Exception): 

56 """Base-exception for all exceptions thrown by pyVersioning.""" 

57 

58 # WORKAROUND: for Python <3.11 

59 # Implementing a dummy method for Python versions before 

60 if version_info < (3, 11): # pragma: no cover 

61 __notes__: List[str] 

62 

63 def add_note(self, message: str) -> None: 

64 try: 

65 self.__notes__.append(message) 

66 except AttributeError: 

67 self.__notes__ = [message] 

68 

69 

70@export 

71class ToolException(VersioningException): 

72 """Exception thrown when a data collection tools has an error.""" 

73 

74 command: str #: Command that caused the exception. 

75 errorMessage: str #: Error message returned by the tool. 

76 

77 def __init__(self, command: str, errorMessage: str) -> None: 

78 """ 

79 

80 :param command: Command that caused the exception. 

81 :param errorMessage: Error message returned by the tool. 

82 """ 

83 super().__init__(errorMessage) 

84 self.command = command 

85 self.errorMessage = errorMessage 

86 

87 

88@export 

89class SelfDescriptive(metaclass=ExtendedType, slots=True, mixin=True): 

90 # TODO: could this be filled with a decorator? 

91 _public: ClassVar[Tuple[str, ...]] 

92 

93 def Keys(self) -> Generator[str, None, None]: 

94 for element in self._public: 

95 yield element 

96 

97 def KeyValuePairs(self) -> Generator[Tuple[str, Any], None, None]: 

98 for element in self._public: 

99 value = self.__getattribute__(element) 

100 yield element, value 

101 

102 

103@export 

104class Tool(SelfDescriptive): 

105 """This data structure class describes the tool name and version of pyVersioning.""" 

106 

107 _name: str #: Name of pyVersioning 

108 _version: SemanticVersion #: Version of the pyVersioning 

109 

110 _public: ClassVar[Tuple[str, ...]] = ("name", "version") 

111 

112 def __init__(self, name: str, version: SemanticVersion) -> None: 

113 """ 

114 Initialize a tool with name and version. 

115 

116 :param name: The tool's name. 

117 :param version: The tool's version. 

118 """ 

119 self._name = name 

120 self._version = version 

121 

122 @readonly 

123 def name(self) -> str: 

124 """ 

125 Read-only property to return the name of the tool. 

126 

127 :return: Name of the tool. 

128 """ 

129 return self._name 

130 

131 @readonly 

132 def version(self) -> SemanticVersion: 

133 """ 

134 Read-only property to return the version of the tool. 

135 

136 :return: Version of the tool as :class:`SemanticVersion`. 

137 """ 

138 return self._version 

139 

140 def __str__(self) -> str: 

141 """ 

142 Return a string representation of this tool description. 

143 

144 :returns: The tool's name and version. 

145 """ 

146 return f"{self._name} - {self._version}" 

147 

148 

149@export 

150class Date(date, SelfDescriptive): 

151 """ 

152 This data structure class describes a date (dd.mm.yyyy). 

153 """ 

154 

155 _public: ClassVar[Tuple[str, ...]] = ("day", "month", "year") 

156 

157 

158@export 

159class Time(time, SelfDescriptive): 

160 """ 

161 This data structure class describes a time (hh:mm:ss). 

162 """ 

163 

164 _public: ClassVar[Tuple[str, ...]] = ("hour", "minute", "second") 

165 

166 

167@export 

168class Person(SelfDescriptive): 

169 """ 

170 This data structure class describes a person with name and email address. 

171 

172 This class is used to describe an author or committer in a version control system. 

173 """ 

174 

175 _name: str #: Name of the person. 

176 _email: str #: Email address of the person. 

177 

178 _public: ClassVar[Tuple[str, ...]] = ("name", "email") 

179 

180 def __init__(self, name: str, email: str) -> None: 

181 """ 

182 Initialize a person with name and email address. 

183 

184 :param name: The person's name. 

185 :param email: The person's email address. 

186 """ 

187 self._name = name 

188 self._email = email 

189 

190 @readonly 

191 def name(self) -> str: 

192 """ 

193 Read-only property to return the name of the person. 

194 

195 :return: Name of the person. 

196 """ 

197 return self._name 

198 

199 @readonly 

200 def email(self) -> str: 

201 """ 

202 Read-only property to return the email address of the person. 

203 

204 :return: Email address of the person. 

205 """ 

206 return self._email 

207 

208 def __str__(self) -> str: 

209 """ 

210 Return a string representation of a person (committer, author, ...). 

211 

212 :returns: The persons name and email address. 

213 """ 

214 return f"{self._name} <{self._email}>" 

215 

216 

217@export 

218class Commit(SelfDescriptive): 

219 """ 

220 This data structure class describes a Git commit with Hash, commit date and time, committer, author and commit 

221 description. 

222 """ 

223 

224 _hash: str 

225 _date: date 

226 _time: time 

227 _author: Person 

228 _committer: Person 

229 _comment: str 

230 _oneline: Union[str, bool] = False 

231 

232 _public: ClassVar[Tuple[str, ...]] = ("hash", "date", "time", "author", "committer", "comment", "oneline") 

233 

234 def __init__(self, hash: str, date: date, time: time, author: Person, committer: Person, comment: str) -> None: 

235 """ 

236 Initialize a Git commit. 

237 

238 :param hash: The commit's hash. 

239 :param date: The commit's date. 

240 :param time: The commit's time. 

241 :param author: The commit's author. 

242 :param committer: The commit's committer. 

243 :param comment: The commit's comment. 

244 """ 

245 self._hash = hash 

246 self._date = date 

247 self._time = time 

248 self._author = author 

249 self._committer = committer 

250 self._comment = comment 

251 

252 if comment != "": 252 ↛ exitline 252 didn't return from function '__init__' because the condition on line 252 was always true

253 self._oneline = comment.split("\n")[0] 

254 

255 @readonly 

256 def hash(self) -> str: 

257 """ 

258 Read-only property to return the hash of the commit. 

259 

260 :return: Hash of the commit as string. 

261 """ 

262 return self._hash 

263 

264 @readonly 

265 def date(self) -> date: 

266 """ 

267 Read-only property to return the date of the commit. 

268 

269 :return: Date of the commit as :class:`date`. 

270 """ 

271 return self._date 

272 

273 @readonly 

274 def time(self) -> time: 

275 """ 

276 Read-only property to return the time of the commit. 

277 

278 :return: Time of the commit as :class:`time`. 

279 """ 

280 return self._time 

281 

282 @readonly 

283 def author(self) -> Person: 

284 """ 

285 Read-only property to return the author of the commit. 

286 

287 :return: Author of the commit as :class:`Person`. 

288 """ 

289 return self._author 

290 

291 @readonly 

292 def committer(self) -> Person: 

293 """ 

294 Read-only property to return the committer of the commit. 

295 

296 :return: Committer of the commit as :class:`Person`. 

297 """ 

298 return self._committer 

299 

300 @readonly 

301 def comment(self) -> str: 

302 """ 

303 Read-only property to return the comment of the commit. 

304 

305 :return: Author of the comment as string. 

306 """ 

307 return self._comment 

308 

309 @readonly 

310 def oneline(self) -> Union[str, bool]: 

311 return self._oneline 

312 

313 def __str__(self) -> str: 

314 """ 

315 Return a string representation of a commit. 

316 

317 :returns: The date, time, committer, hash and one-liner description. 

318 """ 

319 return f"{self._date} {self._time} - {self._committer} - {self._hash} - {self._oneline}" 

320 

321 

322@export 

323class Git(SelfDescriptive): 

324 """ 

325 This data structure class describes all collected data from Git. 

326 """ 

327 _commit: Commit #: Git commit information 

328 _reference: str #: Git reference (branch or tag) 

329 _tag: str #: Git tag 

330 _branch: str #: Git branch 

331 _repository: str #: Git repository URL 

332 

333 _public: ClassVar[Tuple[str, ...]] = ("commit", "reference", "tag", "branch", "repository") 

334 

335 def __init__(self, commit: Commit, repository: str, tag: str = "", branch: str = "") -> None: 

336 self._commit = commit 

337 self._tag = tag 

338 self._branch = branch 

339 self._repository = repository 

340 

341 if tag != "": 341 ↛ 343line 341 didn't jump to line 343 because the condition on line 341 was always true

342 self._reference = tag 

343 elif branch != "": 

344 self._reference = branch 

345 else: 

346 self._reference = "[Detached HEAD]" 

347 

348 @readonly 

349 def commit(self) -> Commit: 

350 """ 

351 Read-only property to return the Git commit. 

352 

353 :return: The commit information as :class:`Commit`. 

354 """ 

355 return self._commit 

356 

357 @readonly 

358 def reference(self) -> str: 

359 """ 

360 Read-only property to return the Git reference. 

361 

362 A reference is either a branch or tag. 

363 

364 :return: The current Git reference as string. 

365 """ 

366 return self._reference 

367 

368 @readonly 

369 def tag(self) -> str: 

370 """ 

371 Read-only property to return the Git tag. 

372 

373 :return: The current Git tag name as string. 

374 """ 

375 return self._tag 

376 

377 @readonly 

378 def branch(self) -> str: 

379 """ 

380 Read-only property to return the Git branch. 

381 

382 :return: The current Git branch name as string. 

383 """ 

384 return self._branch 

385 

386 @readonly 

387 def repository(self) -> str: 

388 """ 

389 Read-only property to return the Git repository URL. 

390 

391 :return: The current Git repository URL as string. 

392 """ 

393 return self._repository 

394 

395 def __str__(self) -> str: 

396 """ 

397 Return a string representation of a repository. 

398 

399 :returns: The date, time, committer, hash and one-liner description. 

400 """ 

401 return f"{self._repository}:{self._reference} - {self._commit._hash} - {self._commit._oneline}" 

402 

403 

404@export 

405class Project(SelfDescriptive): 

406 """This data structure class describes a project.""" 

407 

408 _name: str #: Name of the project. 

409 _variant: str #: Variant of the project. 

410 _version: SemanticVersion #: Version of the project. 

411 

412 _public: ClassVar[Tuple[str, ...]] = ("name", "variant", "version") 

413 

414 def __init__( 

415 self, 

416 name: str, 

417 version: Union[str, SemanticVersion, None] = None, 

418 variant: Nullable[str] = None 

419 ) -> None: 

420 """Assign fields and convert version string to a `Version` object.""" 

421 

422 self._name = name if name is not None else "" 

423 self._variant = variant if variant is not None else "" 

424 

425 if isinstance(version, SemanticVersion): 

426 self._version = version 

427 elif isinstance(version, str): 

428 self._version = SemanticVersion.Parse(version) 

429 elif version is None: 429 ↛ exitline 429 didn't return from function '__init__' because the condition on line 429 was always true

430 self._version = SemanticVersion.Parse("v0.0.0") 

431 

432 @readonly 

433 def name(self) -> str: 

434 """ 

435 Read-only property to return the name of the project. 

436 

437 :return: The project's name as string. 

438 """ 

439 return self._name 

440 

441 @readonly 

442 def variant(self) -> str: 

443 """ 

444 Read-only property to return the variant name of the project. 

445 

446 :return: The project's variant as string. 

447 """ 

448 return self._variant 

449 

450 @readonly 

451 def version(self) -> SemanticVersion: 

452 """ 

453 Read-only property to return the version of the project. 

454 

455 :return: The project's version. 

456 """ 

457 return self._version 

458 

459 def __str__(self) -> str: 

460 """ 

461 Return a string representation of a project. 

462 

463 :returns: The project name, project variant and project version. 

464 """ 

465 return f"{self._name} - {self._variant} {self._version}" 

466 

467 

468@export 

469class Compiler(SelfDescriptive): 

470 _name: str #: Name of the compiler. 

471 _version: SemanticVersion #: Version of the compiler. 

472 _configuration: str #: Compiler configuration. 

473 _options: str #: Compiler options (compiler flags). 

474 

475 _public: ClassVar[Tuple[str, ...]] = ("name", "version", "configuration", "options") 

476 

477 def __init__( 

478 self, 

479 name: str, 

480 version: Union[str, SemanticVersion] = "", 

481 configuration: str = "", 

482 options: str = "" 

483 ) -> None: 

484 """Assign fields and convert version string to a `Version` object.""" 

485 

486 self._name = name if name is not None else "" 

487 self._configuration = configuration if configuration is not None else "" 

488 self._options = options if options is not None else "" 

489 

490 if isinstance(version, SemanticVersion): 490 ↛ 492line 490 didn't jump to line 492 because the condition on line 490 was always true

491 self._version = version 

492 elif isinstance(version, str): 

493 self._version = SemanticVersion.Parse(version) 

494 elif version is None: 

495 self._version = SemanticVersion.Parse("v0.0.0") 

496 

497 @readonly 

498 def name(self) -> str: 

499 """ 

500 Read-only property to return the name of the compiler. 

501 

502 :return: The compiler's name as string. 

503 """ 

504 return self._name 

505 

506 @readonly 

507 def version(self) -> SemanticVersion: 

508 """ 

509 Read-only property to return the version of the compiler. 

510 

511 :return: The compiler's version. 

512 """ 

513 return self._version 

514 

515 @readonly 

516 def configuration(self) -> str: 

517 """ 

518 Read-only property to return the configuration of the compiler. 

519 

520 :return: The compiler's configuration. 

521 """ 

522 return self._configuration 

523 

524 @readonly 

525 def options(self) -> str: 

526 """ 

527 Read-only property to return the options of the compiler. 

528 

529 :return: The compiler's options. 

530 """ 

531 return self._options 

532 

533 def __str__(self) -> str: 

534 """ 

535 Return a string representation of a compiler. 

536 

537 :returns: The compiler name and compiler version. 

538 """ 

539 return f"{self._name} - {self._version}" 

540 

541 

542@export 

543class Build(SelfDescriptive): 

544 _date: date #: Build date. 

545 _time: time #: Build time. 

546 _compiler: Compiler #: Use compiler (and options) for the build. 

547 

548 _public: ClassVar[Tuple[str, ...]] = ("date", "time", "compiler") 

549 

550 def __init__(self, date: date, time: time, compiler: Compiler) -> None: 

551 self._date = date 

552 self._time = time 

553 self._compiler = compiler 

554 

555 @readonly 

556 def date(self) -> date: 

557 """ 

558 Read-only property to return the date of the build. 

559 

560 :return: Date of the build as :class:`date`. 

561 """ 

562 return self._date 

563 

564 @readonly 

565 def time(self) -> time: 

566 """ 

567 Read-only property to return the time of the build. 

568 

569 :return: Time of the build as :class:`date`. 

570 """ 

571 return self._time 

572 

573 @readonly 

574 def compiler(self) -> Compiler: 

575 return self._compiler 

576 

577 

578@export 

579class Platforms(Enum): 

580 """An enumeration of platforms supported by pyTooling.""" 

581 Workstation = auto() #: A local work station, server, PC or laptop. 

582 AppVeyor = auto() #: A CI service operated by `AppVeyor <https://www.appveyor.com/>`__. 

583 GitHub = auto() #: A CI service offered by `GitHub <https://github.com/>`__ called GitHub Actions. 

584 GitLab = auto() #: A CI service offered by `GitLab <https://about.gitlab.com/>`__. 

585 Travis = auto() #: A CI service operated by `Travis <https://www.travis-ci.com/>`__. 

586 

587 

588@export 

589class Platform(SelfDescriptive): 

590 """.. todo:: Platform needs documentation""" 

591 

592 _ciService: str 

593 _public: ClassVar[Tuple[str, ...]] = ('ci_service', ) 

594 

595 def __init__(self, ciService: str) -> None: 

596 self._ciService = ciService 

597 

598 @readonly 

599 def ci_service(self) -> str: 

600 return self._ciService 

601 

602 

603@export 

604class BaseService(metaclass=ExtendedType): 

605 """Base-class to collect platform and environment information from e.g. environment variables.""" 

606 

607 # @abstractmethod 

608 def GetPlatform(self) -> Platform: # type: ignore[empty-body] 

609 """ 

610 .. todo:: 

611 getPlatform needs documentation 

612 

613 """ 

614 

615 

616@export 

617class GitShowCommand(Enum): 

618 CommitDateTime = auto() 

619 CommitAuthorName = auto() 

620 CommitAuthorEmail = auto() 

621 CommitCommitterName = auto() 

622 CommitCommitterEmail = auto() 

623 CommitHash = auto() 

624 CommitComment = auto() 

625 

626 

627@export 

628class GitHelperMixin(metaclass=ExtendedType, mixin=True): 

629 __GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP = { 

630 GitShowCommand.CommitHash: "%H", 

631 GitShowCommand.CommitDateTime: "%ct", 

632 GitShowCommand.CommitAuthorName: "%an", 

633 GitShowCommand.CommitAuthorEmail: "%ae", 

634 GitShowCommand.CommitCommitterName: "%cn", 

635 GitShowCommand.CommitCommitterEmail: "%ce", 

636 GitShowCommand.CommitComment: "%B", 

637 } 

638 

639 def ExecuteGitShow(self, cmd: GitShowCommand, ref: str = "HEAD") -> str: 

640 format = f"--format='{self.__GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP[cmd]}'" 

641 

642 command = "git" 

643 arguments = ("show", "-s", format, ref) 

644 try: 

645 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

646 except CalledProcessError as ex: 

647 raise ToolException(f"{command} {' '.join(arguments)}", str(ex)) 

648 

649 if completed.returncode == 0: 649 ↛ 653line 649 didn't jump to line 653 because the condition on line 649 was always true

650 comment = completed.stdout.decode("utf-8") 

651 return comment[1:-2] 

652 else: 

653 message = completed.stderr.decode("utf-8") 

654 raise ToolException(f"{command} {' '.join(arguments)}", message) 

655 

656 

657@export 

658class Versioning(ILineTerminal, GitHelperMixin): 

659 _variables: Dict[str, Any] 

660 _platform: Platforms = Platforms.Workstation 

661 _service: BaseService 

662 

663 def __init__(self, terminal: ILineTerminal) -> None: 

664 super().__init__(terminal) 

665 

666 self._variables = { 

667 "tool": Tool("pyVersioning", SemanticVersion.Parse(f"v{__version__}")) 

668 } 

669 

670 if "APPVEYOR" in environ: 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true

671 self._platform = Platforms.AppVeyor 

672 elif "GITHUB_ACTIONS" in environ: 672 ↛ 674line 672 didn't jump to line 674 because the condition on line 672 was always true

673 self._platform = Platforms.GitHub 

674 elif "GITLAB_CI" in environ: 

675 self._platform = Platforms.GitLab 

676 elif "TRAVIS" in environ: 

677 self._platform = Platforms.Travis 

678 else: 

679 self._platform = Platforms.Workstation 

680 

681 self.WriteDebug(f"Detected platform: {self._platform.name}") 

682 

683 @readonly 

684 def Variables(self) -> Dict[str, Any]: 

685 return self._variables 

686 

687 @readonly 

688 def Platform(self) -> Platforms: 

689 return self._platform 

690 

691 def LoadDataFromConfiguration(self, config: Configuration) -> None: 

692 """Preload versioning information from a configuration file.""" 

693 

694 self._variables["version"] = self.GetVersion(config.project) 

695 self._variables["project"] = self.GetProject(config.project) 

696 self._variables["build"] = self.GetBuild(config.build) 

697 

698 def CollectData(self) -> None: 

699 """Collect versioning information from environment including CI services (if available).""" 

700 

701 from pyVersioning.AppVeyor import AppVeyor 

702 from pyVersioning.CIService import WorkStation 

703 from pyVersioning.GitLab import GitLab 

704 from pyVersioning.GitHub import GitHub 

705 from pyVersioning.Travis import Travis 

706 

707 if self._platform is Platforms.AppVeyor: 707 ↛ 708line 707 didn't jump to line 708 because the condition on line 707 was never true

708 self._service = AppVeyor() 

709 self._variables["appveyor"] = self._service.GetEnvironment() 

710 elif self._platform is Platforms.GitHub: 710 ↛ 713line 710 didn't jump to line 713 because the condition on line 710 was always true

711 self._service = GitHub() 

712 self._variables["github"] = self._service.GetEnvironment() 

713 elif self._platform is Platforms.GitLab: 

714 self._service = GitLab() 

715 self._variables["gitlab"] = self._service.GetEnvironment() 

716 elif self._platform is Platforms.Travis: 

717 self._service = Travis() 

718 self._variables["travis"] = self._service.GetEnvironment() 

719 else: 

720 self._service = WorkStation() 

721 

722 self._variables["git"] = self.GetGitInformation() 

723 self._variables["platform"] = self._service.GetPlatform() 

724 self._variables["env"] = self.GetEnvironment() 

725 

726 self.CalculateData() 

727 

728 def CalculateData(self) -> None: 

729 if self._variables["git"].tag != "": 729 ↛ exitline 729 didn't return from function 'CalculateData' because the condition on line 729 was always true

730 pass 

731 

732 def GetVersion(self, config: Project) -> SemanticVersion: 

733 if config.version is not None: 733 ↛ 736line 733 didn't jump to line 736 because the condition on line 733 was always true

734 return config.version 

735 else: 

736 return SemanticVersion.Parse("0.0.0") 

737 

738 def GetGitInformation(self) -> Git: 

739 return Git( 

740 commit=self.GetLastCommit(), 

741 tag=self.GetGitTag(), 

742 branch=self.GetGitLocalBranch(), 

743 repository=self.GetGitRemoteURL() 

744 ) 

745 

746 def GetLastCommit(self) -> Commit: 

747 dt = self.GetCommitDate() 

748 

749 return Commit( 

750 hash=self.GetGitHash(), 

751 date=dt.date(), 

752 time=dt.time(), 

753 author=self.GetCommitAuthor(), 

754 committer=self.GetCommitCommitter(), 

755 comment=self.GetCommitComment() 

756 ) 

757 

758 def GetGitHash(self) -> str: 

759 if self._platform is not Platforms.Workstation: 759 ↛ 762line 759 didn't jump to line 762 because the condition on line 759 was always true

760 return self._service.GetGitHash() 

761 

762 return self.ExecuteGitShow(GitShowCommand.CommitHash) 

763 

764 def GetCommitDate(self) -> datetime: 

765 if self._platform is not Platforms.Workstation: 765 ↛ 768line 765 didn't jump to line 768 because the condition on line 765 was always true

766 return self._service.GetCommitDate() 

767 

768 datetimeString = self.ExecuteGitShow(GitShowCommand.CommitDateTime) 

769 return datetime.fromtimestamp(int(datetimeString)) 

770 

771 def GetCommitAuthor(self) -> Person: 

772 return Person( 

773 name=self.GetCommitAuthorName(), 

774 email=self.GetCommitAuthorEmail() 

775 ) 

776 

777 def GetCommitAuthorName(self) -> str: 

778 return self.ExecuteGitShow(GitShowCommand.CommitAuthorName) 

779 

780 def GetCommitAuthorEmail(self) -> str: 

781 return self.ExecuteGitShow(GitShowCommand.CommitAuthorEmail) 

782 

783 def GetCommitCommitter(self) -> Person: 

784 return Person( 

785 name=self.GetCommitCommitterName(), 

786 email=self.GetCommitCommitterEmail() 

787 ) 

788 

789 def GetCommitCommitterName(self) -> str: 

790 return self.ExecuteGitShow(GitShowCommand.CommitCommitterName) 

791 

792 def GetCommitCommitterEmail(self) -> str: 

793 return self.ExecuteGitShow(GitShowCommand.CommitCommitterEmail) 

794 

795 def GetCommitComment(self) -> str: 

796 return self.ExecuteGitShow(GitShowCommand.CommitComment) 

797 

798 def GetGitLocalBranch(self) -> str: 

799 if self._platform is not Platforms.Workstation: 799 ↛ 802line 799 didn't jump to line 802 because the condition on line 799 was always true

800 return self._service.GetGitBranch() 

801 

802 command = "git" 

803 arguments = ("branch", "--show-current") 

804 try: 

805 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

806 except CalledProcessError: 

807 return "" 

808 

809 if completed.returncode == 0: 

810 return completed.stdout.decode("utf-8").split("\n")[0] 

811 else: 

812 message = completed.stderr.decode("utf-8") 

813 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

814 

815 def GetGitRemoteBranch(self, localBranch: Nullable[str] = None) -> str: 

816 if self._platform is not Platforms.Workstation: 

817 return self._service.GetGitBranch() 

818 

819 if localBranch is None: 

820 localBranch = self.GetGitLocalBranch() 

821 

822 command = "git" 

823 arguments = ("config", f"branch.{localBranch}.merge") 

824 try: 

825 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

826 except CalledProcessError: 

827 raise Exception() # XXX: needs error message 

828 

829 if completed.returncode == 0: 

830 return completed.stdout.decode("utf-8").split("\n")[0] 

831 else: 

832 message = completed.stderr.decode("utf-8") 

833 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

834 raise Exception() # XXX: needs error message 

835 

836 def GetGitRemote(self, localBranch: Nullable[str] = None) -> str: 

837 if localBranch is None: 

838 localBranch = self.GetGitLocalBranch() 

839 

840 command = "git" 

841 arguments = ("config", f"branch.{localBranch}.remote") 

842 try: 

843 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

844 except CalledProcessError: 

845 raise Exception() # XXX: needs error message 

846 

847 if completed.returncode == 0: 

848 return completed.stdout.decode("utf-8").split("\n")[0] 

849 elif completed.returncode == 1: 

850 self.WriteWarning(f"Branch '{localBranch}' is not pushed to a remote.") 

851 return f"(local) {localBranch}" 

852 else: 

853 message = completed.stderr.decode("utf-8") 

854 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

855 raise Exception() # XXX: needs error message 

856 

857 def GetGitTag(self) -> str: 

858 if self._platform is not Platforms.Workstation: 858 ↛ 861line 858 didn't jump to line 861 because the condition on line 858 was always true

859 return self._service.GetGitTag() 

860 

861 command = "git" 

862 arguments = ("tag", "--points-at", "HEAD") 

863 try: 

864 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

865 except CalledProcessError: 

866 raise Exception() # XXX: needs error message 

867 

868 if completed.returncode == 0: 

869 return completed.stdout.decode("utf-8").split("\n")[0] 

870 else: 

871 message = completed.stderr.decode("utf-8") 

872 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

873 raise Exception() # XXX: needs error message 

874 

875 def GetGitRemoteURL(self, remote: Nullable[str] = None) -> str: 

876 if self._platform is not Platforms.Workstation: 876 ↛ 879line 876 didn't jump to line 879 because the condition on line 876 was always true

877 return self._service.GetGitRepository() 

878 

879 if remote is None: 

880 remote = self.GetGitRemote() 

881 

882 command = "git" 

883 arguments = ("config", f"remote.{remote}.url") 

884 try: 

885 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE) 

886 except CalledProcessError: 

887 raise Exception() # XXX: needs error message 

888 

889 if completed.returncode == 0: 

890 return completed.stdout.decode("utf-8").split("\n")[0] 

891 else: 

892 message = completed.stderr.decode("utf-8") 

893 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}") 

894 raise Exception() # XXX: needs error message 

895 

896 # self.WriteFatal(f"Message from '{command}': {message}") 

897 

898 def GetProject(self, config: Project) -> Project: 

899 return Project( 

900 name=config.name, 

901 version=config.version, 

902 variant=config.variant 

903 ) 

904 

905 def GetBuild(self, config: Build) -> Build: 

906 dt = datetime.now() 

907 return Build( 

908 date=dt.date(), 

909 time=dt.time(), 

910 compiler=self.GetCompiler(config.compiler) 

911 ) 

912 

913 def GetCompiler(self, config: Compiler) -> Compiler: 

914 return Compiler( 

915 name=config.name, 

916 version=SemanticVersion.Parse(config.version), 

917 configuration=config.configuration, 

918 options=config.options 

919 ) 

920 

921 def GetEnvironment(self) -> Any: 

922 env = {} 

923 for key, value in environ.items(): 

924 if not key.isidentifier(): 

925 self.WriteWarning(f"Skipping environment variable '{key}', because it's not a valid Python identifier.") 

926 continue 

927 key = key.replace("(", "_") 

928 key = key.replace(")", "_") 

929 key = key.replace(" ", "_") 

930 env[key] = value 

931 

932 def func(s) -> Generator[Tuple[str, Any], None, None]: 

933 for e in env.keys(): 

934 yield e, s.__getattribute__(e) 

935 

936 Environment = make_dataclass( 

937 "Environment", 

938 [(name, str) for name in env.keys()], 

939# bases=(SelfDescriptive,), 

940 namespace={ 

941 "as_dict": lambda self: env, 

942 "Keys": lambda self: env.keys(), 

943 "KeyValuePairs": lambda self: func(self) 

944 }, 

945 repr=True 

946 ) 

947 

948 return Environment(**env) 

949 

950 def FillOutTemplate(self, template: str, **kwargs) -> str: 

951 # apply variables 

952 try: 

953 return template.format(**self._variables, **kwargs) 

954 except AttributeError as ex: 

955 self.WriteFatal(f"Syntax error in template. Accessing field '{ex.name}' of '{ex.obj.__class__.__name__}'.")