Coverage for gsb/history.py: 100%
67 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 21:03 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 21:03 +0000
1"""Functionality for tracking and managing revision history"""
3import datetime as dt
4import logging
5from collections import defaultdict
6from pathlib import Path
7from typing import Any, TypedDict
9from . import _git
10from .logging import IMPORTANT
12LOGGER = logging.getLogger(__name__)
15class Revision(TypedDict):
16 """Metadata on a GSB-managed version
18 Parameters
19 ----------
20 identifier : str
21 A short, unique identifier for the revision
22 commit_hash : str
23 The full hexadecimal hash associated with the revision
24 description : str
25 A description of the version
26 timestamp : dt.datetime
27 The time at which the version was created
28 tagged : bool
29 Whether or not this is a tagged revision
30 gsb : bool
31 Whether or not this is a GSB-created revision
32 """
34 identifier: str
35 commit_hash: str
36 description: str
37 timestamp: dt.datetime
38 tagged: bool
39 gsb: bool
42def get_history(
43 repo_root: Path,
44 tagged_only: bool = True,
45 include_non_gsb: bool = False,
46 limit: int = -1,
47 since: dt.date = dt.datetime(1970, 1, 1),
48 since_last_tagged_backup: bool = False,
49 always_include_latest: bool = False,
50) -> list[Revision]:
51 """Retrieve a list of GSB-managed versions
53 Parameters
54 ----------
55 repo_root : Path
56 The directory containing the GSB-managed repo
57 tagged_only : bool, optional
58 By default, this method only returns tagged backups. To include
59 all available revisions, pass in `tagged_only=False`.
60 include_non_gsb : bool, optional
61 By default, this method excludes any revisions created outside of `gsb`.
62 To include all git commits and tags, pass in `include_non_gsb=True`.
63 limit : int, optional
64 By default, this method returns the entire history. To return only the
65 last _n_ revisions, pass in `limit=n`.
66 since : date or timestamp, optional
67 By default, this method returns the entire history. To return only
68 revisions made on or after a certain date, pass in `since=<start_date>`.
69 since_last_tagged_backup: bool, optional
70 False by default. To return only revisions made since the last tagged
71 backup, pass in `since_last_tagged_backup=True` (and, presumably,
72 `tagged_only=False`). This flag is compatible with all other filters.
73 always_include_latest: bool, optional
74 Whether to always include the latest backup, whether or not it's
75 tagged or GSB-managed, and ignoring any other options. Default is False.
77 Returns
78 -------
79 list of dict
80 metadata on the requested revisions, sorted in reverse-chronological
81 order
83 Raises
84 ------
85 OSError
86 If the specified repo does not exist or is not a git repo
87 ValueError
88 If called with `return_parent=True` `get_history` and the earliest commit
89 had no parent (being the initial commit, itself).
90 """
91 tag_lookup = defaultdict(list)
92 tag_count = 0
93 for tag in _git.get_tags(repo_root, annotated_only=True):
94 tag_lookup[tag.target].append(tag)
95 tag_count += 1
96 LOGGER.debug("Retrieved %s tags", tag_count)
98 revisions: list[Revision] = []
99 defer_break = False
100 for commit in _git.log(repo_root):
101 if len(revisions) == limit or commit.timestamp < since:
102 if always_include_latest:
103 defer_break = True
104 else:
105 break
106 if tags := tag_lookup.get(commit):
107 if since_last_tagged_backup:
108 break
109 tagged = True
110 for tag in reversed(tags):
111 identifier = tag.name
112 is_gsb = tag.gsb if tag.gsb is not None else commit.gsb
113 description = tag.annotation or commit.message
114 if is_gsb or include_non_gsb:
115 revisions.append(
116 {
117 "identifier": identifier,
118 "commit_hash": commit.hash,
119 "description": description.strip(),
120 "timestamp": commit.timestamp,
121 "tagged": tagged,
122 "gsb": is_gsb,
123 }
124 )
125 else:
126 if tagged_only and not always_include_latest:
127 continue
128 tagged = False
129 identifier = commit.hash[:8]
130 is_gsb = commit.gsb
131 description = commit.message
132 if not include_non_gsb and not is_gsb and not always_include_latest:
133 continue
134 revisions.append(
135 {
136 "identifier": identifier,
137 "commit_hash": commit.hash,
138 "description": description.strip(),
139 "timestamp": commit.timestamp,
140 "tagged": tagged,
141 "gsb": is_gsb,
142 }
143 )
144 if always_include_latest:
145 if defer_break:
146 break
147 always_include_latest = False
148 return revisions
151def log_revision(revision: Revision, idx: int | None) -> None:
152 """Print (log) a revision
154 Parameters
155 ----------
156 revision : dict
157 Metadata for the revision
158 idx : int | None
159 The index to give to the revision. If None is specified, the revision
160 will be displayed with a "-" instead of a numbering.
162 Notes
163 -----
164 - The version identifiers and dates are logged at the IMPORTANT (verbose=0) level
165 - The version descriptions are logged at the INFO (verbose=1) level
166 - The full version hashes are logged at the DEBUG (verbose=2) level
167 """
168 args: list[Any] = [revision["identifier"], revision["timestamp"].isoformat("-")]
169 if idx is None:
170 format_string = "- %s from %s"
171 else:
172 format_string = "%d. %s from %s"
173 args.insert(0, idx)
175 LOGGER.log(IMPORTANT, format_string, *args)
177 LOGGER.debug("Full reference: %s", revision["commit_hash"])
178 LOGGER.info("%s", revision["description"])
181def show_history(
182 repo_root: Path,
183 numbering: int | None = 1,
184 **kwargs,
185) -> list[Revision]:
186 """Fetch and print (log) the list of versions for the specified repo matching
187 the given specs
189 Parameters
190 ----------
191 repo_root : Path
192 The directory containing the GSB-managed repo
193 numbering: int or None, optional
194 When displaying the versions, the default behavior is to number the
195 results, starting at 1. To set a different starting number, provide that.
196 To use "-" instead of numbers, pass in `numbering=None`.
197 **kwargs
198 Any other options will be passed directly to `get_history()`
199 method
201 Notes
202 -----
203 See `log_revision()` for details about what information is logged to each
204 log level
206 Returns
207 -------
208 list of dict
209 metadata on the requested revisions, sorted in reverse-chronological
210 order
212 Raises
213 ------
214 OSError
215 If the specified repo does not exist or is not a git repo
216 """
217 history = get_history(repo_root, **kwargs)
218 for i, revision in enumerate(history):
219 log_revision(revision, i + numbering if numbering is not None else None)
220 return history