Coverage for gsb/history.py: 100%

59 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-08 20:16 +0000

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

2 

3import datetime as dt 

4import logging 

5from pathlib import Path 

6from typing import Any, TypedDict 

7 

8from . import _git 

9from .logging import IMPORTANT 

10 

11LOGGER = logging.getLogger(__name__) 

12 

13 

14class Revision(TypedDict): 

15 """Metadata on a GSB-managed version 

16 

17 Parameters 

18 ---------- 

19 identifier : str 

20 A short, unique identifier for the revision 

21 commit_hash : str 

22 The full hexadecimal hash associated with the revision 

23 description : str 

24 A description of the version 

25 timestamp : dt.datetime 

26 The time at which the version was created 

27 tagged : bool 

28 Whether or not this is a tagged revision 

29 gsb : bool 

30 Whether or not this is a GSB-created revision 

31 """ 

32 

33 identifier: str 

34 commit_hash: str 

35 description: str 

36 timestamp: dt.datetime 

37 tagged: bool 

38 gsb: bool 

39 

40 

41def get_history( 

42 repo_root: Path, 

43 tagged_only: bool = True, 

44 include_non_gsb: bool = False, 

45 limit: int = -1, 

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

47 since_last_tagged_backup: bool = False, 

48 always_include_latest: bool = False, 

49) -> list[Revision]: 

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

51 

52 Parameters 

53 ---------- 

54 repo_root : Path 

55 The directory containing the GSB-managed repo 

56 tagged_only : bool, optional 

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

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

59 include_non_gsb : bool, optional 

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

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

62 limit : int, optional 

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

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

65 since : date or timestamp, optional 

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

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

68 since_last_tagged_backup: bool, optional 

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

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

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

72 always_include_latest: bool, optional 

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

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

75 

76 Returns 

77 ------- 

78 list of dict 

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

80 order 

81 

82 Raises 

83 ------ 

84 OSError 

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

86 ValueError 

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

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

89 """ 

90 tag_lookup = { 

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

92 } 

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

94 

95 revisions: list[Revision] = [] 

96 defer_break = False 

97 for commit in _git.log(repo_root): 

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

99 if always_include_latest: 

100 defer_break = True 

101 else: 

102 break 

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

104 if since_last_tagged_backup: 

105 break 

106 tagged = True 

107 identifier = tag.name 

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

109 description = tag.annotation or commit.message 

110 else: 

111 if tagged_only and not always_include_latest: 

112 continue 

113 tagged = False 

114 identifier = commit.hash[:8] 

115 is_gsb = commit.gsb 

116 description = commit.message 

117 if not include_non_gsb and not is_gsb and not always_include_latest: 

118 continue 

119 revisions.append( 

120 { 

121 "identifier": identifier, 

122 "commit_hash": commit.hash, 

123 "description": description.strip(), 

124 "timestamp": commit.timestamp, 

125 "tagged": tagged, 

126 "gsb": is_gsb, 

127 } 

128 ) 

129 if always_include_latest: 

130 if defer_break: 

131 break 

132 always_include_latest = False 

133 return revisions 

134 

135 

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

137 """Print (log) a revision 

138 

139 Parameters 

140 ---------- 

141 revision : dict 

142 Metadata for the revision 

143 idx : int | None 

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

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

146 

147 Notes 

148 ----- 

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

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

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

152 """ 

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

154 if idx is None: 

155 format_string = "- %s from %s" 

156 else: 

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

158 args.insert(0, idx) 

159 

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

161 

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

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

164 

165 

166def show_history( 

167 repo_root: Path, 

168 numbering: int | None = 1, 

169 **kwargs, 

170) -> list[Revision]: 

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

172 the given specs 

173 

174 Parameters 

175 ---------- 

176 repo_root : Path 

177 The directory containing the GSB-managed repo 

178 numbering: int or None, optional 

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

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

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

182 **kwargs 

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

184 method 

185 

186 Notes 

187 ----- 

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

189 log level 

190 

191 Returns 

192 ------- 

193 list of dict 

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

195 order 

196 

197 Raises 

198 ------ 

199 OSError 

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

201 """ 

202 history = get_history(repo_root, **kwargs) 

203 for i, revision in enumerate(history): 

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

205 return history