Coverage for gsb/history.py: 100%
59 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-08 20:16 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-08 20:16 +0000
1"""Functionality for tracking and managing revision history"""
3import datetime as dt
4import logging
5from pathlib import Path
6from typing import Any, TypedDict
8from . import _git
9from .logging import IMPORTANT
11LOGGER = logging.getLogger(__name__)
14class Revision(TypedDict):
15 """Metadata on a GSB-managed version
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 """
33 identifier: str
34 commit_hash: str
35 description: str
36 timestamp: dt.datetime
37 tagged: bool
38 gsb: bool
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
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.
76 Returns
77 -------
78 list of dict
79 metadata on the requested revisions, sorted in reverse-chronological
80 order
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))
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
136def log_revision(revision: Revision, idx: int | None) -> None:
137 """Print (log) a revision
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.
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)
160 LOGGER.log(IMPORTANT, format_string, *args)
162 LOGGER.debug("Full reference: %s", revision["commit_hash"])
163 LOGGER.info("%s", revision["description"])
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
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
186 Notes
187 -----
188 See `log_revision()` for details about what information is logged to each
189 log level
191 Returns
192 -------
193 list of dict
194 metadata on the requested revisions, sorted in reverse-chronological
195 order
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