gitmole 0.6.12__tar.gz → 0.6.13__tar.gz
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.
- {gitmole-0.6.12 → gitmole-0.6.13}/PKG-INFO +1 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/__init__.py +1 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/filetypes.py +2 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/findings.py +2 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/identity.py +1 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/leaks.py +10 -3
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/maat.py +6 -5
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/render.py +2 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole.egg-info/PKG-INFO +1 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_filetypes.py +5 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_findings.py +10 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_identity.py +2 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_leaks.py +19 -1
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_maat.py +8 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_render.py +13 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/LICENSE +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/README.md +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/__main__.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/backtest.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/banner.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/blame.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/clean.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/cli.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/coupling.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/functions.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/hotspots.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/knowledge.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/load.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/loss.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/run.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/textfmt.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/trend.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole/watch.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole.egg-info/SOURCES.txt +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole.egg-info/dependency_links.txt +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole.egg-info/entry_points.txt +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole.egg-info/requires.txt +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/gitmole.egg-info/top_level.txt +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/pyproject.toml +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/setup.cfg +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_backtest.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_banner.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_blame.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_clean.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_cli.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_coupling.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_functions.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_golden.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_hotspots.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_knowledge.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_load.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_loss.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_packaging.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_run.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_textfmt.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_trend.py +0 -0
- {gitmole-0.6.12 → gitmole-0.6.13}/tests/test_watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitmole
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.13
|
|
4
4
|
Summary: Offline git repository analysis with a terminal report: hotspots, coupling, ownership, code age, secrets, repo health.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/antvinni/gitmole
|
|
@@ -190,6 +190,7 @@ def is_release(path: str, plumbing=frozenset()) -> bool:
|
|
|
190
190
|
# What a generated file says about itself in its first lines: protoc, ajv, code generators of every kind.
|
|
191
191
|
_GENERATED = re.compile(r"auto[- ]?generated|generated (by|from|file|code|automatically|with)|do not (edit|modify)|@generated|code generated", re.I)
|
|
192
192
|
GENERATED_HEAD_LINES = 5
|
|
193
|
+
_GENERATED_NAME = re.compile(r"\.(min\.js|min\.css|bundle\.js|map)$", re.I) # a build output by name: nobody edits a bundle or a source map
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
def _generated_patterns(repo: str) -> list:
|
|
@@ -219,7 +220,7 @@ def generated_files(repo: str, paths: list) -> list:
|
|
|
219
220
|
patterns = _generated_patterns(repo)
|
|
220
221
|
out = []
|
|
221
222
|
for path in paths:
|
|
222
|
-
if any(_attribute_match(path, p) for p in patterns):
|
|
223
|
+
if _GENERATED_NAME.search(path) or any(_attribute_match(path, p) for p in patterns):
|
|
223
224
|
out.append(path)
|
|
224
225
|
continue
|
|
225
226
|
try:
|
|
@@ -194,9 +194,10 @@ def tight_coupling(report: dict, min_degree: int = 80, min_revs: int = 5) -> lis
|
|
|
194
194
|
"""A file and its test are expected to change together, so pairs with a test file on either side are
|
|
195
195
|
left out; so are pairs where either file is no longer in the tree, which are history, not a dependency,
|
|
196
196
|
and pairs of release plumbing (two version files, a manifest and its lock file), which are a release."""
|
|
197
|
-
tree, vendored = _tree(report), filetypes.vendor_dirs(report)
|
|
197
|
+
tree, vendored, derived = _tree(report), filetypes.vendor_dirs(report), _generated(report)
|
|
198
198
|
pairs = [p for p in report.get("coupling") or [] if p["degree"] >= min_degree and p["average-revs"] >= min_revs
|
|
199
199
|
and not (filetypes.is_test_path(p["entity"]) or filetypes.is_test_path(p["coupled"]))
|
|
200
|
+
and not (p["entity"] in derived or p["coupled"] in derived)
|
|
200
201
|
and not (filetypes.is_release_path(p["entity"]) and filetypes.is_release_path(p["coupled"]))
|
|
201
202
|
and not filetypes.is_header_pair(p["entity"], p["coupled"])
|
|
202
203
|
and not (filetypes.is_vendored(p["entity"], vendored) or filetypes.is_vendored(p["coupled"], vendored))
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
_BOT_WORDS = ("dependabot", "renovate", "github-actions", "github actions", "copilot", "cursor agent", "cursor-agent", "cursoragent")
|
|
8
|
-
_BOT_NAME = re.compile(r"\bbot\b|\bci\b|deploy|automation", re.I) # "Deploy from CI", "Release Bot", "
|
|
8
|
+
_BOT_NAME = re.compile(r"\bbot\b|\bci\b|deploy|automation|releaser|release-bot", re.I) # "Deploy from CI", "Release Bot", "hugoreleaser"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def is_bot(name: str, email: str = "") -> bool:
|
|
@@ -39,7 +39,8 @@ RAW_FIELDS = ("Secret", "Match", "Line", "Message", "Attributes")
|
|
|
39
39
|
_VERSION = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$")
|
|
40
40
|
_MARKER = re.compile(r"^(<[^<>]+>|x+|(?:x{2,}[-_ ]?)+|your[-_][\w-]+|change[-_]?me|replace[-_]?me)$", re.I)
|
|
41
41
|
_EXAMPLE_WORDS = {"password", "passwd", "pass", "secret", "hello", "hey", "test", "example", "sample", "dummy", "foo", "bar",
|
|
42
|
-
"baz", "admin", "root", "user", "123456", "12345678", "123456789", "abc123", "qwerty", "letmein", "welcome"
|
|
42
|
+
"baz", "admin", "root", "user", "123456", "12345678", "123456789", "abc123", "qwerty", "letmein", "welcome",
|
|
43
|
+
"x-oauth-basic", "x-access-token", "x-token-auth"} # documented literals for the password slot of token auth
|
|
43
44
|
# A whole value that refers to an environment variable or a template field is where the secret will be
|
|
44
45
|
# read from, not the secret: `@env:AC_PASSWORD`, `${DB_PASSWORD}`, `{{.Env.X}}`, `process.env.X`, `<%= ENV['X'] %>`.
|
|
45
46
|
_ENV_REF = re.compile(r"^(@env:\w+|\$\{[^{}]+\}|\$[A-Za-z_]\w*|\$\([^()]+\)|%[A-Za-z_]\w*%|\{\{.*\}\}|<%=?.*%>"
|
|
@@ -78,14 +79,20 @@ def _made_up(value: str) -> bool:
|
|
|
78
79
|
return letters == sorted(letters) and digits == sorted(digits) and len(set(letters)) >= 4 or (not letters and digits == sorted(digits))
|
|
79
80
|
|
|
80
81
|
|
|
82
|
+
# A template field anywhere inside the value: {token}, ${X}, ${{ X }}, %(name)s, {{ x }}, <user>.
|
|
83
|
+
_TEMPLATE_FIELD = re.compile(r"\{[A-Za-z_][\w.]*\}|\$\{[^{}]*\}|\$\{\{.*?\}\}|%\([A-Za-z_]\w*\)s|\{\{.*?\}\}|<[A-Za-z_][\w-]*>")
|
|
84
|
+
# Five or more words of letters: a description of the secret, not the secret.
|
|
85
|
+
_PROSE = re.compile(r"^[A-Za-z][A-Za-z'-]*(\s+[A-Za-z][A-Za-z'-]*){4,}\s*$")
|
|
86
|
+
|
|
87
|
+
|
|
81
88
|
def is_placeholder(value: str, line: str = "") -> bool:
|
|
82
89
|
"""Whether `value` has a shape that cannot be a live secret. `line` is the source line the value
|
|
83
90
|
sat on, read from the clone at scan time and never written."""
|
|
84
91
|
value = (value or "").strip()
|
|
85
92
|
if (_VERSION.match(value) or value.endswith("...") or value.endswith("…") or _MARKER.match(value) or value.lower() in _EXAMPLE_WORDS
|
|
86
|
-
or _ENV_REF.match(value) or _made_up(value)):
|
|
93
|
+
or _ENV_REF.match(value) or _made_up(value) or _TEMPLATE_FIELD.search(value) or _PROSE.match(value)):
|
|
87
94
|
return True
|
|
88
|
-
if line and _EXAMPLE_LINE.search(line):
|
|
95
|
+
if line and (_EXAMPLE_LINE.search(line) or _TEMPLATE_FIELD.search(line)): # the line is an example, or a template being filled in
|
|
89
96
|
return True
|
|
90
97
|
m = _KEY_BLOCK.search(value)
|
|
91
98
|
if m:
|
|
@@ -221,15 +221,16 @@ def activity(commits: list) -> dict:
|
|
|
221
221
|
"reverted": dict(sorted(reverted.items(), key=lambda kv: (-kv[1], kv[0])))}
|
|
222
222
|
|
|
223
223
|
|
|
224
|
-
def plumbing(commits: list, min_revs: int = 20, share: float = 0.8, max_lines: int =
|
|
225
|
-
"""Files whose commits nearly always
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
def plumbing(commits: list, min_revs: int = 20, share: float = 0.8, max_lines: int = 3) -> list:
|
|
225
|
+
"""Files whose commits nearly always swap a few lines for as many: a version constant in
|
|
226
|
+
__init__.py, the three fields of a version struct. Their churn is the release cadence, not where
|
|
227
|
+
the next bug lands. A commit that adds lines without removing any is growth, not a bump. Needs
|
|
228
|
+
enough commits to judge by."""
|
|
228
229
|
revs, tiny = Counter(), Counter()
|
|
229
230
|
for c in commits:
|
|
230
231
|
for path, added, deleted in c["files"]:
|
|
231
232
|
revs[path] += 1
|
|
232
|
-
if added
|
|
233
|
+
if added == deleted and added <= max_lines:
|
|
233
234
|
tiny[path] += 1
|
|
234
235
|
return [{"entity": e, "n-revs": n, "tiny-revs": tiny[e]} for e, n in sorted(revs.items()) if n >= min_revs and tiny[e] / n >= share]
|
|
235
236
|
|
|
@@ -443,7 +443,8 @@ def coupling_section(report: dict, full: bool = True, width=None) -> dict:
|
|
|
443
443
|
pairs, release_note = _hide_release(pairs, full)
|
|
444
444
|
pairs, header_note = _hide_header_pairs(pairs, full)
|
|
445
445
|
pairs, vendor_note = _hide_vendor(pairs, lambda p: (p["entity"], p["coupled"]), full, noun="vendored pair", plural="vendored pairs", report=report)
|
|
446
|
-
|
|
446
|
+
pairs, generated_note = _hide_generated(pairs, lambda p: (p["entity"], p["coupled"]), report, full, noun="generated pair", plural="generated pairs")
|
|
447
|
+
gone_note = _join_hidden(gone_note, release_note, header_note, vendor_note, generated_note)
|
|
447
448
|
groups, cluster_note = [], None
|
|
448
449
|
if full is not True:
|
|
449
450
|
# a directory whose files all change together is one row; --full lists every pair
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitmole
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.13
|
|
4
4
|
Summary: Offline git repository analysis with a terminal report: hotspots, coupling, ownership, code age, secrets, repo health.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/antvinni/gitmole
|
|
@@ -133,6 +133,9 @@ class TestPaths(unittest.TestCase):
|
|
|
133
133
|
"docs/notes.md": "generated notes are the best notes\n",
|
|
134
134
|
"dist/bundle.js": "var a = 1;\n",
|
|
135
135
|
"src/late.py": "\n" * 10 + "# generated by hand, do not edit\n", # a marker past the first lines does not count
|
|
136
|
+
"internal/js/renderkatex.bundle.js": "var a=1;\n", # a bundle, a minified file or a source map is a build output by name
|
|
137
|
+
"static/app.min.css": "a{b:c}\n",
|
|
138
|
+
"static/app.js.map": "{}\n",
|
|
136
139
|
}
|
|
137
140
|
for path, text in files.items():
|
|
138
141
|
os.makedirs(os.path.join(d, os.path.dirname(path)), exist_ok=True)
|
|
@@ -141,7 +144,8 @@ class TestPaths(unittest.TestCase):
|
|
|
141
144
|
with open(os.path.join(d, ".gitattributes"), "w") as fh:
|
|
142
145
|
fh.write("* text=auto\ndist/* linguist-generated=true\n*.min.js linguist-generated\n")
|
|
143
146
|
found = filetypes.generated_files(d, sorted(files))
|
|
144
|
-
self.assertEqual(found, ["dist/bundle.js", "gen/schema.py", "lib/config-validator.js", "pb/api.pb.go"
|
|
147
|
+
self.assertEqual(found, ["dist/bundle.js", "gen/schema.py", "internal/js/renderkatex.bundle.js", "lib/config-validator.js", "pb/api.pb.go",
|
|
148
|
+
"static/app.js.map", "static/app.min.css"])
|
|
145
149
|
|
|
146
150
|
def test_a_nested_licence_with_other_copyright_holders_marks_a_vendored_tree(self):
|
|
147
151
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -261,6 +261,16 @@ class TightCoupling(unittest.TestCase):
|
|
|
261
261
|
f = findings.tight_coupling(report(coupling=pairs))
|
|
262
262
|
self.assertIn("2 pairs", f[0]["detail"], "without a tree listing every pair counts")
|
|
263
263
|
|
|
264
|
+
def test_generated_pairs_are_the_generators_coupling(self):
|
|
265
|
+
pairs = [{"entity": "js/greet.bundle.js", "coupled": "js/renderkatex.bundle.js", "degree": 83, "average-revs": 10},
|
|
266
|
+
{"entity": "js/renderkatex.bundle.js", "coupled": "js/renderkatex.js", "degree": 83, "average-revs": 10},
|
|
267
|
+
{"entity": "src/a.js", "coupled": "src/b.js", "degree": 85, "average-revs": 10}]
|
|
268
|
+
r = report(coupling=pairs)
|
|
269
|
+
r["meta"]["generated"] = ["js/greet.bundle.js", "js/renderkatex.bundle.js"]
|
|
270
|
+
f = findings.tight_coupling(r)
|
|
271
|
+
self.assertIn("1 pair changes together", f[0]["detail"])
|
|
272
|
+
self.assertIn("src/a.js", f[0]["detail"])
|
|
273
|
+
|
|
264
274
|
def test_vendored_pairs_are_somebody_elses_coupling(self):
|
|
265
275
|
pairs = [{"entity": "deps/hiredis/adapters/ae.h", "coupled": "deps/hiredis/adapters/libev.h", "degree": 84, "average-revs": 10},
|
|
266
276
|
{"entity": "src/ae.c", "coupled": "deps/hiredis/net.h", "degree": 85, "average-revs": 10},
|
|
@@ -98,7 +98,8 @@ class IsBot(unittest.TestCase):
|
|
|
98
98
|
("dependabot[bot]", "support@github.com"), ("Dependabot", "dependabot@example.com"),
|
|
99
99
|
("Renovate Bot", "bot@renovateapp.com"), ("GitHub Actions", "actions@github.com"),
|
|
100
100
|
("Copilot", "198982749+Copilot@users.noreply.github.com"), ("Cursor Agent", "cursoragent@cursor.com"),
|
|
101
|
-
("Deploy from CI", ""), ("Release Bot", "release@x.com"), ("CI", "ci@x.com"), ("Homebrew Automation", "a@x.com")
|
|
101
|
+
("Deploy from CI", ""), ("Release Bot", "release@x.com"), ("CI", "ci@x.com"), ("Homebrew Automation", "a@x.com"),
|
|
102
|
+
("hugoreleaser", "hugoreleaser@x.com"), ("semantic-release-bot", "s@x.com")]:
|
|
102
103
|
self.assertTrue(identity.is_bot(name, email), (name, email))
|
|
103
104
|
|
|
104
105
|
def test_people_are_not_bots(self):
|
|
@@ -61,8 +61,11 @@ class Placeholder(unittest.TestCase):
|
|
|
61
61
|
|
|
62
62
|
def test_common_example_words_are_placeholders(self):
|
|
63
63
|
# `password: 'hello'` in a doc comment, `secret` in a sample config: the words every example uses
|
|
64
|
-
for value in ["hello", "Hello", "secret", "password", "PASSWORD", "example", "123456", "qwerty", "letmein", "foo", "dummy"
|
|
64
|
+
for value in ["hello", "Hello", "secret", "password", "PASSWORD", "example", "123456", "qwerty", "letmein", "foo", "dummy",
|
|
65
|
+
"x-oauth-basic", "x-access-token"]: # GitHub's documented literals for the password slot of token auth
|
|
65
66
|
self.assertTrue(leaks.is_placeholder(value), value)
|
|
67
|
+
self.assertTrue(leaks.is_placeholder("hunter2", line='url = f"https://{token}:hunter2@github.com/{SLUG}.git"'),
|
|
68
|
+
"a line with a template field is a template being filled in")
|
|
66
69
|
for value in ["hello123", "secret-9f8a7b6c5d4e", "s3cr3t!Passw0rd", "foobarbaz2024"]:
|
|
67
70
|
self.assertFalse(leaks.is_placeholder(value), "a word inside other material is not a placeholder: " + value)
|
|
68
71
|
|
|
@@ -110,6 +113,21 @@ class Placeholder(unittest.TestCase):
|
|
|
110
113
|
for value in ["nz5ej2kypkvcw0rn5cvhs6qxtm", "abcdefg", "zyxwvutsrq", "ghp_" + "a1" * 18]:
|
|
111
114
|
self.assertFalse(leaks.is_placeholder(value), value)
|
|
112
115
|
|
|
116
|
+
def test_a_value_with_a_template_field_inside_it_is_a_template(self):
|
|
117
|
+
# pytest's release script: oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
|
|
118
|
+
for value in ["https://{token}:x-oauth-basic@github.com/pytest-dev/pytest.git", "Bearer ${TOKEN}", "key-%(api_key)s", "sk_live_{{ secret }}",
|
|
119
|
+
"${{ secrets.CODECOV_TOKEN }}", "https://<user>:<pass>@host/db"]:
|
|
120
|
+
self.assertTrue(leaks.is_placeholder(value), value)
|
|
121
|
+
for value in ["https://x:hunter2@host/db", "ghp_" + "a1" * 18, "{" + "a1" * 10]:
|
|
122
|
+
self.assertFalse(leaks.is_placeholder(value), value)
|
|
123
|
+
|
|
124
|
+
def test_a_sentence_of_prose_is_a_description_not_a_secret(self):
|
|
125
|
+
# pytest's devpi task: 'password': 'user password on devpi to stage the generated package '
|
|
126
|
+
for value in ["user password on devpi to stage the generated package", "The API key used to talk to the billing service"]:
|
|
127
|
+
self.assertTrue(leaks.is_placeholder(value), value)
|
|
128
|
+
for value in ["correct horse battery", "nz5ej2kypkvcw0rn5cvhs6qxtm", "one two 3 four five"]:
|
|
129
|
+
self.assertFalse(leaks.is_placeholder(value), "under five words, or a digit among them: not prose")
|
|
130
|
+
|
|
113
131
|
def test_anything_else_is_taken_seriously(self):
|
|
114
132
|
# built at runtime: a literal in these shapes would trip secret scanners on this very file
|
|
115
133
|
key_id, long_key = "AKIA" + "X" * 16, "6L" + "x" * 38
|
|
@@ -111,6 +111,14 @@ class Plumbing(unittest.TestCase):
|
|
|
111
111
|
self.assertEqual(rows, [{"entity": "fastapi/__init__.py", "n-revs": 331, "tiny-revs": 300}],
|
|
112
112
|
"routing.py has real edits; VERSION has too few commits to judge")
|
|
113
113
|
|
|
114
|
+
def test_a_bump_that_swaps_three_lines_is_tiny_but_growth_is_not(self):
|
|
115
|
+
# hugo's version_current.go: every release edits Major, Minor and PatchLevel, 3 lines out and 3 in
|
|
116
|
+
bumps = [{"hash": f"b{i}", "date": "2026-01-01", "time": "", "author": "A", "subject": "release", "files": [("common/hugo/version_current.go", 3, 3)]}
|
|
117
|
+
for i in range(30)]
|
|
118
|
+
growth = [{"hash": f"g{i}", "date": "2026-01-01", "time": "", "author": "A", "subject": "add", "files": [("lib/list.py", 3, 0)]} for i in range(30)]
|
|
119
|
+
rows = maat.plumbing(bumps + growth)
|
|
120
|
+
self.assertEqual([r["entity"] for r in rows], ["common/hugo/version_current.go"], "three lines added with nothing removed is growth, not a bump")
|
|
121
|
+
|
|
114
122
|
def test_written_alongside_the_other_analyses(self):
|
|
115
123
|
with tempfile.TemporaryDirectory() as d:
|
|
116
124
|
log = os.path.join(d, "log.txt")
|
|
@@ -368,6 +368,19 @@ class Report(unittest.TestCase):
|
|
|
368
368
|
self.assertNotIn("deps/", coupling)
|
|
369
369
|
self.assertIn("2 vendored pairs hidden; --full shows them", coupling)
|
|
370
370
|
|
|
371
|
+
def test_default_coupling_hides_generated_pairs_and_says_so(self):
|
|
372
|
+
r = sample_report()
|
|
373
|
+
r["meta"]["generated"] = ["js/a.bundle.js", "js/b.bundle.js"]
|
|
374
|
+
for f in ("js/a.bundle.js", "js/b.bundle.js", "js/b.js", "src/x.js", "src/y.js"):
|
|
375
|
+
r["size"]["files"][f] = {"code": 30, "complexity": 1}
|
|
376
|
+
r["coupling"] = [{"entity": "js/a.bundle.js", "coupled": "js/b.bundle.js", "degree": 83, "average-revs": 20},
|
|
377
|
+
{"entity": "js/b.bundle.js", "coupled": "js/b.js", "degree": 83, "average-revs": 20},
|
|
378
|
+
{"entity": "src/x.js", "coupled": "src/y.js", "degree": 60, "average-revs": 9}]
|
|
379
|
+
coupling = _section_text(rendered(r, [], width=200), "Change coupling")
|
|
380
|
+
self.assertIn("src/y.js", coupling)
|
|
381
|
+
self.assertNotIn("bundle", coupling)
|
|
382
|
+
self.assertIn("2 generated pairs hidden; --full shows them", coupling)
|
|
383
|
+
|
|
371
384
|
def test_default_coupling_hides_header_pairs_and_says_so(self):
|
|
372
385
|
r = sample_report()
|
|
373
386
|
for f in ("src/vector.c", "src/vector.h", "src/list.c"):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|