Coverage for tsfpga/svn_utils.py: 76%

55 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-21 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) -> 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 """ 

30 check_that_svn_commands_are_available(cwd=cwd) 

31 

32 result = f"r{get_svn_revision(cwd=cwd)}" 

33 

34 if svn_local_changes_are_present(cwd=cwd): 

35 result += " (local changes present)" 

36 

37 return result 

38 

39 

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

41 """ 

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

43 

44 Arguments: 

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

46 """ 

47 try: 

48 get_svn_revision(cwd=cwd) 

49 except (subprocess.CalledProcessError, FileNotFoundError): 

50 return False 

51 return True 

52 

53 

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

55 """ 

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

57 SVN repo. 

58 

59 Arguments: 

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

61 """ 

62 if not svn_commands_are_available(cwd): 

63 message = ( 

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

65 ) 

66 raise RuntimeError(message) 

67 

68 

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

70 """ 

71 Get the current SVN revision number. 

72 

73 Arguments: 

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

75 """ 

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

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

78 

79 # Remove trailing newline 

80 return int(stdout.strip()) 

81 

82 

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

84 """ 

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

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

87 

88 Arguments: 

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

90 """ 

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

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

93 

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

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

96 return regexp.search(stdout) is not None 

97 

98 

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

100 

101 

102def find_svn_files( 

103 directory: Path, 

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

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

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

107) -> Iterable[Path]: 

108 """ 

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

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

111 an SVN server. 

112 

113 Arguments: 

114 directory: Search in this directory. 

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

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

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

118 

119 Return: 

120 The files that are available in SVN. 

121 """ 

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

123 

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

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

126 

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

128 match = RE_SVN_STATUS_LINE.match(line) 

129 if not match: 

130 continue 

131 

132 svn_file = match.group(1) 

133 file_path = directory / svn_file 

134 

135 if not file_path.exists(): 

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

137 

138 if file_path.is_dir(): 

139 continue 

140 

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

142 continue 

143 

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

145 continue 

146 

147 if file_is_in_directory(file_path, excludes): 

148 continue 

149 

150 yield file_path