Coverage for gsb/history.py: 100%

59 statements  

« prev     ^ index     » next       coverage.py v7.2.6, created at 2024-09-08 16:23 -0400

1"""Functionality for tracking and managing revision history""" 

2import datetime as dt 

3import logging 

4from pathlib import Path 

5from typing import Any, TypedDict 

6 

7from . import _git 

8from .logging import IMPORTANT 

9 

10LOGGER = logging.getLogger(__name__) 

11 

12 

13class Revision(TypedDict): 

14 """Metadata on a GSB-managed version 

15 

16 Parameters 

17 ---------- 

18 identifier : str 

19 A short, unique identifier for the revision 

20 commit_hash : str 

21 The full hexadecimal hash associated with the revision 

22 description : str 

23 A description of the version 

24 timestamp : dt.datetime 

25 The time at which the version was created 

26 tagged : bool 

27 Whether or not this is a tagged revision 

28 gsb : bool 

29 Whether or not this is a GSB-created revision 

30 """ 

31 

32 identifier: str 

33 commit_hash: str 

34 description: str 

35 timestamp: dt.datetime 

36 tagged: bool 

37 gsb: bool 

38 

39 

40def get_history( 

41 repo_root: Path, 

42 tagged_only: bool = True, 

43 include_non_gsb: bool = False, 

44 limit: int = -1, 

45 since: dt.date = dt.datetime(1970, 1, 1), 

46 since_last_tagged_backup: bool = False, 

47 always_include_latest: bool = False, 

48) -> list[Revision]: 

49 """Retrieve a list of GSB-managed versions 

50 

51 Parameters 

52 ---------- 

53 repo_root : Path 

54 The directory containing the GSB-managed repo 

55 tagged_only : bool, optional 

56 By default, this method only returns tagged backups. To include 

57 all available revisions, pass in `tagged_only=False`. 

58 include_non_gsb : bool, optional 

59 By default, this method excludes any revisions created outside of `gsb`. 

60 To include all git commits and tags, pass in `include_non_gsb=True`. 

61 limit : int, optional 

62 By default, this method returns the entire history. To return only the 

63 last _n_ revisions, pass in `limit=n`. 

64 since : date or timestamp, optional 

65 By default, this method returns the entire history. To return only 

66 revisions made on or after a certain date, pass in `since=<start_date>`. 

67 since_last_tagged_backup: bool, optional 

68 False by default. To return only revisions made since the last tagged 

69 backup, pass in `since_last_tagged_backup=True` (and, presumably, 

70 `tagged_only=False`). This flag is compatible with all other filters. 

71 always_include_latest: bool, optional 

72 Whether to always include the latest backup, whether or not it's 

73 tagged or GSB-managed, and ignoring any other options. Default is False. 

74 

75 Returns 

76 ------- 

77 list of dict 

78 metadata on the requested revisions, sorted in reverse-chronological 

79 order 

80 

81 Raises 

82 ------ 

83 OSError 

84 If the specified repo does not exist or is not a git repo 

85 ValueError 

86 If called with `return_parent=True` `get_history` and the earliest commit 

87 had no parent (being the initial commit, itself). 

88 """ 

89 tag_lookup = { 

90 tag.target: tag for tag in _git.get_tags(repo_root, annotated_only=True) 

91 } 

92 LOGGER.debug("Retrieved %s tags", len(tag_lookup)) 

93 

94 revisions: list[Revision] = [] 

95 defer_break = False 

96 for commit in _git.log(repo_root): 

97 if len(revisions) == limit or commit.timestamp < since: 

98 if always_include_latest: 

99 defer_break = True 

100 else: 

101 break 

102 if tag := tag_lookup.get(commit): 

103 if since_last_tagged_backup: 

104 break 

105 tagged = True 

106 identifier = tag.name 

107 is_gsb = tag.gsb if tag.gsb is not None else commit.gsb 

108 description = tag.annotation or commit.message 

109 else: 

110 if tagged_only and not always_include_latest: 

111 continue 

112 tagged = False 

113 identifier = commit.hash[:8] 

114 is_gsb = commit.gsb 

115 description = commit.message 

116 if not include_non_gsb and not is_gsb and not always_include_latest: 

117 continue 

118 revisions.append( 

119 { 

120 "identifier": identifier, 

121 "commit_hash": commit.hash, 

122 "description": description.strip(), 

123 "timestamp": commit.timestamp, 

124 "tagged": tagged, 

125 "gsb": is_gsb, 

126 } 

127 ) 

128 if always_include_latest: 

129 if defer_break: 

130 break 

131 always_include_latest = False 

132 return revisions 

133 

134 

135def log_revision(revision: Revision, idx: int | None) -> None: 

136 """Print (log) a revision 

137 

138 Parameters 

139 ---------- 

140 revision : dict 

141 Metadata for the revision 

142 idx : int | None 

143 The index to give to the revision. If None is specified, the revision 

144 will be displayed with a "-" instead of a numbering. 

145 

146 Notes 

147 ----- 

148 - The version identifiers and dates are logged at the IMPORTANT (verbose=0) level 

149 - The version descriptions are logged at the INFO (verbose=1) level 

150 - The full version hashes are logged at the DEBUG (verbose=2) level 

151 """ 

152 args: list[Any] = [revision["identifier"], revision["timestamp"].isoformat("-")] 

153 if idx is None: 

154 format_string = "- %s from %s" 

155 else: 

156 format_string = "%d. %s from %s" 

157 args.insert(0, idx) 

158 

159 LOGGER.log(IMPORTANT, format_string, *args) 

160 

161 LOGGER.debug("Full reference: %s", revision["commit_hash"]) 

162 LOGGER.info("%s", revision["description"]) 

163 

164 

165def show_history( 

166 repo_root: Path, 

167 numbering: int | None = 1, 

168 **kwargs, 

169) -> list[Revision]: 

170 """Fetch and print (log) the list of versions for the specified repo matching 

171 the given specs 

172 

173 Parameters 

174 ---------- 

175 repo_root : Path 

176 The directory containing the GSB-managed repo 

177 numbering: int or None, optional 

178 When displaying the versions, the default behavior is to number the 

179 results, starting at 1. To set a different starting number, provide that. 

180 To use "-" instead of numbers, pass in `numbering=None`. 

181 **kwargs 

182 Any other options will be passed directly to `get_history()` 

183 method 

184 

185 Notes 

186 ----- 

187 See `log_revision()` for details about what information is logged to each 

188 log level 

189 

190 Returns 

191 ------- 

192 list of dict 

193 metadata on the requested revisions, sorted in reverse-chronological 

194 order 

195 

196 Raises 

197 ------ 

198 OSError 

199 If the specified repo does not exist or is not a git repo 

200 """ 

201 history = get_history(repo_root, **kwargs) 

202 for i, revision in enumerate(history): 

203 log_revision(revision, i + numbering if numbering is not None else None) 

204 return history