Coverage for tsfpga/svn_utils.py: 74%

57 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-01 20:51 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9from __future__ import annotations 

10 

11import re 

12import subprocess 

13from typing import TYPE_CHECKING 

14 

15from .system_utils import file_is_in_directory, run_command 

16 

17if TYPE_CHECKING: 

18 from collections.abc import Iterable 

19 from pathlib import Path 

20 

21 

22def get_svn_revision_information(cwd: Path | None = None, use_rst_annotation: bool = False) -> str: 

23 """ 

24 Get a string describing the current SVN commit. 

25 E.g. ``"r1234"`` or ``"r1234 (local changes present)"``. 

26 

27 Arguments: 

28 cwd: The directory where SVN commands will be run. 

29 use_rst_annotation: Use reStructuredText literal annotation for the revision value. 

30 """ 

31 check_that_svn_commands_are_available(cwd=cwd) 

32 

33 annotation = "``" if use_rst_annotation else "" 

34 revision = f"r{get_svn_revision(cwd=cwd)}" 

35 result = f"{annotation}{revision}{annotation}" 

36 

37 if svn_local_changes_are_present(cwd=cwd): 

38 result += " (local changes present)" 

39 

40 return result 

41 

42 

43def svn_commands_are_available(cwd: Path | None = None) -> bool: 

44 """ 

45 True if "svn" command executable is available and ``cwd`` is in a valid SVN repo. 

46 

47 Arguments: 

48 cwd: The directory where SVN commands will be run. 

49 """ 

50 try: 

51 get_svn_revision(cwd=cwd) 

52 except (subprocess.CalledProcessError, FileNotFoundError): 

53 return False 

54 return True 

55 

56 

57def check_that_svn_commands_are_available(cwd: Path | None = None) -> None: 

58 """ 

59 Raise an exception unless "svn" command executable is available and ``cwd`` is in a valid 

60 SVN repo. 

61 

62 Arguments: 

63 cwd: The directory where SVN commands will be run. 

64 """ 

65 if not svn_commands_are_available(cwd): 

66 message = ( 

67 "Could not run SVN. Is the command available on PATH? Is the script called from a repo?" 

68 ) 

69 raise RuntimeError(message) 

70 

71 

72def get_svn_revision(cwd: Path | None = None) -> int: 

73 """ 

74 Get the current SVN revision number. 

75 

76 Arguments: 

77 cwd: The directory where SVN commands will be run. 

78 """ 

79 command = ["svn", "info", "--show-item", "revision"] 

80 stdout: str = run_command(cmd=command, cwd=cwd, capture_output=True).stdout 

81 

82 # Remove trailing newline 

83 return int(stdout.strip()) 

84 

85 

86def svn_local_changes_are_present(cwd: Path | None = None) -> bool: 

87 """ 

88 Return true if the repo contains changes that have been made after the last commit. 

89 Info from here: https://rubyinrails.com/2014/01/11/svn-command-to-check-modified-files/ 

90 

91 Arguments: 

92 cwd: The directory where SVN commands will be run. 

93 """ 

94 command = ["svn", "status"] 

95 stdout: str = run_command(cmd=command, cwd=cwd, capture_output=True).stdout 

96 

97 # Status code for file Added, Deleted, Modified, in Conflict or missing 

98 regexp = re.compile(r"\n[ADMC!] ") 

99 return regexp.search(stdout) is not None 

100 

101 

102RE_SVN_STATUS_LINE = re.compile(r"^.+\d+\s+\d+\s+\S+\s+(\S+)$") 

103 

104 

105def find_svn_files( 

106 directory: Path, 

107 excludes: list[Path] | None = None, 

108 file_endings_include: str | tuple[str, ...] | None = None, 

109 file_endings_avoid: str | tuple[str, ...] | None = None, 

110) -> Iterable[Path]: 

111 """ 

112 Find files that are checked in to SVN. It runs "svn status" rather than "svn ls". This means 

113 that it is a local operation, that does not require credentials or any connection with 

114 an SVN server. 

115 

116 Arguments: 

117 directory: Search in this directory. 

118 excludes: These files and folders will not be included. 

119 file_endings_include: Only files with these endings will be included. 

120 file_endings_avoid: Files with these endings will not be included. 

121 

122 Return: 

123 The files that are available in SVN. 

124 """ 

125 excludes = [] if excludes is None else [exclude.resolve() for exclude in excludes] 

126 

127 command = ["svn", "status", "-v"] 

128 stdout = run_command(cmd=command, cwd=directory, capture_output=True).stdout 

129 

130 for line in stdout.split("\n"): 

131 match = RE_SVN_STATUS_LINE.match(line) 

132 if not match: 

133 continue 

134 

135 svn_file = match.group(1) 

136 file_path = directory / svn_file 

137 

138 if not file_path.exists(): 

139 raise FileNotFoundError(f"Error when concatenating relative paths: {file_path}") 

140 

141 if file_path.is_dir(): 

142 continue 

143 

144 if file_endings_include is not None and not file_path.name.endswith(file_endings_include): 

145 continue 

146 

147 if file_endings_avoid is not None and file_path.name.endswith(file_endings_avoid): 

148 continue 

149 

150 if file_is_in_directory(file_path, excludes): 

151 continue 

152 

153 yield file_path