reqctl 0.1.38__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reqctl/__init__.py +0 -0
- reqctl/baseline.py +380 -0
- reqctl/cite.py +1229 -0
- reqctl/cli.py +1514 -0
- reqctl/corpus.py +1283 -0
- reqctl/fields.py +361 -0
- reqctl/graph.py +207 -0
- reqctl/history.py +175 -0
- reqctl/portal.html +2213 -0
- reqctl/portal.py +131 -0
- reqctl/reach.py +425 -0
- reqctl/resolve.py +74 -0
- reqctl/schemas/baseline.schema.yaml +42 -0
- reqctl/schemas/dictionary.schema.yaml +256 -0
- reqctl/schemas/guard.schema.yaml +131 -0
- reqctl/schemas/requirement.schema.yaml +152 -0
- reqctl/settings.py +183 -0
- reqctl/typescript.py +143 -0
- reqctl/uncited.py +257 -0
- reqctl/validate.py +1929 -0
- reqctl/write.py +2004 -0
- reqctl-0.1.38.dist-info/METADATA +11 -0
- reqctl-0.1.38.dist-info/RECORD +26 -0
- reqctl-0.1.38.dist-info/WHEEL +5 -0
- reqctl-0.1.38.dist-info/entry_points.txt +2 -0
- reqctl-0.1.38.dist-info/top_level.txt +1 -0
reqctl/history.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from . import corpus
|
|
5
|
+
from .corpus import FOLDERS, ReqctlError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _git(root, *args, given=b"", absent=None):
|
|
9
|
+
try:
|
|
10
|
+
found = subprocess.run(["git", *args], cwd=root, input=given,
|
|
11
|
+
capture_output=True, check=False)
|
|
12
|
+
except OSError as error:
|
|
13
|
+
# @req> REQ-30898926@Vu28eWaG_1mE vvgdbf
|
|
14
|
+
raise ReqctlError(f"cannot run git: {error.strerror} -- install git or "
|
|
15
|
+
"put it on PATH") from error
|
|
16
|
+
if found.returncode not in (0, absent):
|
|
17
|
+
# @req> REQ-30898926@Vu28eWaG_1mE vcr2ti
|
|
18
|
+
raise ReqctlError(f"cannot read the history: git {args[0]}: "
|
|
19
|
+
f"{found.stderr.decode(errors='replace').strip()} -- "
|
|
20
|
+
"run inside a git work tree, or repair the "
|
|
21
|
+
"repository (`git fsck` names the damage)")
|
|
22
|
+
return None if found.returncode else found.stdout
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _uid(path):
|
|
26
|
+
parts = path.split("/")
|
|
27
|
+
if len(parts) == 3 and parts[2].endswith((".yml", ".yaml")) and not parts[2].startswith("."):
|
|
28
|
+
return parts[2].rpartition(".")[0]
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# @req> REQ-82027237@R-XinrGfTNzL q4dejo
|
|
33
|
+
# @req> REQ-73796163@qg5x9Wvk_AEp bdp26a
|
|
34
|
+
def carried(uid, data, blocks):
|
|
35
|
+
return corpus.stamp_of(uid, data), {
|
|
36
|
+
**{key: corpus.entry_stamp_of(uid, data, key) for key in corpus.entries(data) or {}},
|
|
37
|
+
**{block: corpus.held_block(data, block) for block in blocks}}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _flat(value):
|
|
41
|
+
return " ".join(corpus.plain(value).split())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# @req> REQ-50929621@tSdGG-tCMu8G dvvyvi
|
|
45
|
+
# @req> REQ-24608716@rrTjNN2Ay0D0 w7fcu5
|
|
46
|
+
def lines(row):
|
|
47
|
+
width = max(len(key) for key in row["now"])
|
|
48
|
+
return "".join(
|
|
49
|
+
f"\n {key.replace('_', ' '):<{width}} pinned "
|
|
50
|
+
f"{'unknown' if row['then'] is None else _flat(row['then'].get(key))}"
|
|
51
|
+
f"\n {'':<{width}} now {_flat(held)}"
|
|
52
|
+
for key, held in row["now"].items())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class History:
|
|
56
|
+
def __init__(self, root, kinds=FOLDERS):
|
|
57
|
+
self.root = root
|
|
58
|
+
self.folders = [f"requirements/{FOLDERS[kind]}" for kind in kinds]
|
|
59
|
+
self._rows, self._head = None, {}
|
|
60
|
+
self._blobs, self._parsed, self._states = {}, {}, {}
|
|
61
|
+
|
|
62
|
+
def _load(self):
|
|
63
|
+
if self._rows is not None:
|
|
64
|
+
return
|
|
65
|
+
if _git(self.root, "rev-parse", "-q", "--verify", "HEAD", absent=1) is None:
|
|
66
|
+
self._rows = {}
|
|
67
|
+
return
|
|
68
|
+
# @req+ REQ-59306961@EGz7Ll3e9FKA dm7ihn
|
|
69
|
+
shallow = Path(self.root) / _git(self.root, "rev-parse", "--git-path",
|
|
70
|
+
"shallow").decode().strip()
|
|
71
|
+
grafted = set(shallow.read_text().split()) if shallow.is_file() else set()
|
|
72
|
+
# @req- dm7ihn
|
|
73
|
+
rows, commit = {}, None
|
|
74
|
+
# @req+ REQ-82027237@R-XinrGfTNzL efmrhh
|
|
75
|
+
for line in _git(self.root, "log", "--full-history", "--no-show-signature", "-c",
|
|
76
|
+
"--raw", "--root", "--no-renames", "--no-abbrev",
|
|
77
|
+
"--format=%H %ct %cI", "HEAD", "--",
|
|
78
|
+
*self.folders).decode(errors="replace").splitlines():
|
|
79
|
+
if not line.startswith(":"):
|
|
80
|
+
if line:
|
|
81
|
+
sha, when, iso = line.split()
|
|
82
|
+
commit = sha, int(when), None if sha in grafted else iso
|
|
83
|
+
continue
|
|
84
|
+
meta, _, path = line.partition("\t")
|
|
85
|
+
uid = _uid(path)
|
|
86
|
+
if uid is None:
|
|
87
|
+
continue
|
|
88
|
+
count = len(meta) - len(meta.lstrip(":"))
|
|
89
|
+
shas = [sha if sha.strip("0") else None
|
|
90
|
+
for sha in meta.split()[count + 1:2 * count + 2]]
|
|
91
|
+
row = rows.setdefault(uid, {}).setdefault(
|
|
92
|
+
commit[0], [*commit[1:], None, [None] * count])
|
|
93
|
+
row[2] = row[2] or shas[count]
|
|
94
|
+
row[3] = [was or held for was, held in zip(row[3], shas[:count])]
|
|
95
|
+
ordered = {uid: sorted(held.values(), key=lambda row: -row[0])
|
|
96
|
+
for uid, held in rows.items()}
|
|
97
|
+
# @req- efmrhh
|
|
98
|
+
head = {_uid(path): meta.split()[2] for meta, _, path in (
|
|
99
|
+
line.partition("\t") for line in _git(
|
|
100
|
+
self.root, "ls-tree", "-r", "HEAD", "--",
|
|
101
|
+
*self.folders).decode(errors="replace").splitlines())}
|
|
102
|
+
wanted = {sha for held in ordered.values() for row in held
|
|
103
|
+
for sha in (row[2], *row[3]) if sha} | set(head.values())
|
|
104
|
+
out = _git(self.root, "cat-file", "--batch",
|
|
105
|
+
given="".join(f"{sha}\n" for sha in wanted).encode())
|
|
106
|
+
at = 0
|
|
107
|
+
for sha in wanted:
|
|
108
|
+
end = out.index(b"\n", at)
|
|
109
|
+
_, kind, *size = out[at:end].decode().split()
|
|
110
|
+
if not size:
|
|
111
|
+
# @req> REQ-30898926@Vu28eWaG_1mE hz3bqm
|
|
112
|
+
raise ReqctlError(
|
|
113
|
+
f"cannot read the history: git cat-file: {sha} {kind} -- "
|
|
114
|
+
"fetch it with `git fetch origin`, or repair the "
|
|
115
|
+
"repository (`git fsck` names the damage)")
|
|
116
|
+
at = end + 2 + int(size[0])
|
|
117
|
+
if kind == "blob":
|
|
118
|
+
self._blobs[sha] = out[end + 1:at - 1]
|
|
119
|
+
self._head, self._rows = head, ordered
|
|
120
|
+
|
|
121
|
+
def _held(self, uid, sha):
|
|
122
|
+
if sha not in self._parsed:
|
|
123
|
+
try:
|
|
124
|
+
held = corpus.loads(self._blobs[sha].decode(), f"{uid} at {sha}",
|
|
125
|
+
aliases=False)
|
|
126
|
+
except (KeyError, UnicodeDecodeError, ReqctlError):
|
|
127
|
+
held = None
|
|
128
|
+
self._parsed[sha] = held if isinstance(held, dict) else None
|
|
129
|
+
return self._parsed[sha]
|
|
130
|
+
|
|
131
|
+
def _carried(self, uid, sha, blocks):
|
|
132
|
+
if (uid, sha) not in self._states:
|
|
133
|
+
held = self._held(uid, sha)
|
|
134
|
+
self._states[uid, sha] = None if held is None else carried(uid, held, blocks)
|
|
135
|
+
return self._states[uid, sha]
|
|
136
|
+
|
|
137
|
+
# @req> REQ-59306961@EGz7Ll3e9FKA jw6cdl
|
|
138
|
+
# @req> REQ-82027237@R-XinrGfTNzL aum3ke
|
|
139
|
+
# @req> REQ-73796163@qg5x9Wvk_AEp x43h3h
|
|
140
|
+
def updated(self, uid, data):
|
|
141
|
+
self._load()
|
|
142
|
+
blocks = corpus.held_blocks(self.root, uid, data)
|
|
143
|
+
now = carried(uid, data, blocks)
|
|
144
|
+
if self._carried(uid, self._head.get(uid), blocks) != now:
|
|
145
|
+
return None
|
|
146
|
+
return next((iso for _, iso, new, parents in self._rows.get(uid, ())
|
|
147
|
+
if self._carried(uid, new, blocks) == now
|
|
148
|
+
and all(self._carried(uid, parent, blocks) != now
|
|
149
|
+
for parent in parents)),
|
|
150
|
+
None)
|
|
151
|
+
|
|
152
|
+
# @req> REQ-50929621@tSdGG-tCMu8G qgp2gz
|
|
153
|
+
# @req> REQ-24608716@rrTjNN2Ay0D0 xyk6zv
|
|
154
|
+
def _beside(self, uid, data, pinned, stamped, fields):
|
|
155
|
+
then = None
|
|
156
|
+
if isinstance(pinned, str):
|
|
157
|
+
self._load()
|
|
158
|
+
then = next((fields(held) for row in self._rows.get(uid, ())
|
|
159
|
+
if (held := self._held(uid, row[2])) is not None
|
|
160
|
+
and stamped(held) == pinned), None)
|
|
161
|
+
return {"then": then, "now": fields(data)}
|
|
162
|
+
|
|
163
|
+
# @req> REQ-24608716@rrTjNN2Ay0D0 vjkk2b
|
|
164
|
+
def cited(self, uid, data, entry, pinned):
|
|
165
|
+
return self._beside(
|
|
166
|
+
uid, data, pinned,
|
|
167
|
+
lambda held: corpus.cited_stamp(corpus.Item(uid, None, held), entry),
|
|
168
|
+
lambda held: corpus.cited_fields(uid, held, entry))
|
|
169
|
+
|
|
170
|
+
# @req> REQ-24608716@rrTjNN2Ay0D0 dzvsq3
|
|
171
|
+
def pinned(self, uid, data, rest, pinned):
|
|
172
|
+
return self._beside(
|
|
173
|
+
uid, data, pinned,
|
|
174
|
+
lambda held: corpus.address_stamp_of(uid, held, rest),
|
|
175
|
+
lambda held: corpus.covered(uid, held, rest))
|