Coverage for gsb/history.py: 100%
59 statements
« prev ^ index » next coverage.py v7.2.6, created at 2024-09-08 16:23 -0400
« 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
7from . import _git
8from .logging import IMPORTANT
10LOGGER = logging.getLogger(__name__)
13class Revision(TypedDict):
14 """Metadata on a GSB-managed version
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 """
32 identifier: str
33 commit_hash: str
34 description: str
35 timestamp: dt.datetime
36 tagged: bool
37 gsb: bool
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
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.
75 Returns
76 -------
77 list of dict
78 metadata on the requested revisions, sorted in reverse-chronological
79 order
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))
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
135def log_revision(revision: Revision, idx: int | None) -> None:
136 """Print (log) a revision
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.
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)
159 LOGGER.log(IMPORTANT, format_string, *args)
161 LOGGER.debug("Full reference: %s", revision["commit_hash"])
162 LOGGER.info("%s", revision["description"])
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
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
185 Notes
186 -----
187 See `log_revision()` for details about what information is logged to each
188 log level
190 Returns
191 -------
192 list of dict
193 metadata on the requested revisions, sorted in reverse-chronological
194 order
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