gitmole 0.6.7__tar.gz → 0.6.9__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.7 → gitmole-0.6.9}/PKG-INFO +1 -1
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/__init__.py +1 -1
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/findings.py +33 -3
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/functions.py +12 -2
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/identity.py +20 -6
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/leaks.py +6 -1
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/load.py +10 -1
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole.egg-info/PKG-INFO +1 -1
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_findings.py +23 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_functions.py +11 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_identity.py +10 -1
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_leaks.py +8 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_load.py +13 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/LICENSE +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/README.md +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/__main__.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/backtest.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/banner.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/blame.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/clean.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/cli.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/coupling.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/filetypes.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/hotspots.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/knowledge.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/loss.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/maat.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/render.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/run.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/textfmt.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/trend.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole/watch.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole.egg-info/SOURCES.txt +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole.egg-info/dependency_links.txt +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole.egg-info/entry_points.txt +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole.egg-info/requires.txt +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/gitmole.egg-info/top_level.txt +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/pyproject.toml +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/setup.cfg +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_backtest.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_banner.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_blame.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_clean.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_cli.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_coupling.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_filetypes.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_golden.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_hotspots.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_knowledge.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_loss.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_maat.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_packaging.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_render.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_run.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_textfmt.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/tests/test_trend.py +0 -0
- {gitmole-0.6.7 → gitmole-0.6.9}/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.9
|
|
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
|
|
@@ -216,9 +216,39 @@ def tight_coupling(report: dict, min_degree: int = 80, min_revs: int = 5) -> lis
|
|
|
216
216
|
f"Review {first['entity']} and {first['coupled']} first: a shared layout or a hidden dependency links them.")]
|
|
217
217
|
|
|
218
218
|
|
|
219
|
+
def _months_apart(earlier: str, later: str) -> int:
|
|
220
|
+
"""Whole months from one ISO date to another."""
|
|
221
|
+
y1, m1, d1 = (int(x) for x in earlier[:10].split("-"))
|
|
222
|
+
y2, m2, d2 = (int(x) for x in later[:10].split("-"))
|
|
223
|
+
return (y2 - y1) * 12 + (m2 - m1) - (1 if d2 < d1 else 0)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _dormant_months(report: dict) -> int:
|
|
227
|
+
"""Months since the last commit, against the run's reference date (GITMOLE_NOW or today)."""
|
|
228
|
+
import datetime as _dt
|
|
229
|
+
last = report["meta"].get("last_date")
|
|
230
|
+
if not last:
|
|
231
|
+
return 0
|
|
232
|
+
now = report["meta"].get("now") or _dt.date.today().isoformat()
|
|
233
|
+
return max(0, _months_apart(last, now))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def dormant(report: dict, months: int = 12) -> list:
|
|
237
|
+
"""No commits for a year or more: everything else in the report describes a repository that has
|
|
238
|
+
stopped, which is the first thing to know about it."""
|
|
239
|
+
idle = _dormant_months(report)
|
|
240
|
+
if idle < months:
|
|
241
|
+
return []
|
|
242
|
+
return [_f("warning", "Dormant repository", f"No commits since {report['meta']['last_date']}, {idle} months ago.",
|
|
243
|
+
"The rest of the report describes a repository that has stopped; look for a successor or an archive notice before depending on it.")]
|
|
244
|
+
|
|
245
|
+
|
|
219
246
|
def stale_files(report: dict, months: int = 12, share: float = 0.3) -> list:
|
|
220
247
|
"""Files still in the tree that nobody has touched. The age table covers every path in the
|
|
221
|
-
history, so paths that were deleted are left out here; they are not dead code, they are gone.
|
|
248
|
+
history, so paths that were deleted are left out here; they are not dead code, they are gone.
|
|
249
|
+
In a dormant repository every file is untouched because nothing is; the dormancy finding says so."""
|
|
250
|
+
if _dormant_months(report) >= months:
|
|
251
|
+
return []
|
|
222
252
|
age = report.get("age") or []
|
|
223
253
|
tree = _tree(report)
|
|
224
254
|
if tree:
|
|
@@ -467,8 +497,8 @@ def duplication(report: dict, min_lines: int = 30) -> list:
|
|
|
467
497
|
f"Extract the {first['lines']}-line block {where} first.")]
|
|
468
498
|
|
|
469
499
|
|
|
470
|
-
RULES = [secrets_found, placeholder_identity, bus_factor, sizer_concerns, hotspot_dominance, bug_magnets, reverts, brain_methods,
|
|
471
|
-
tight_coupling, duplication, stale_files, knowledge_islands, knowledge_loss]
|
|
500
|
+
RULES = [dormant, secrets_found, placeholder_identity, bus_factor, sizer_concerns, hotspot_dominance, bug_magnets, reverts, brain_methods,
|
|
501
|
+
complexity_growth, tight_coupling, duplication, stale_files, knowledge_islands, knowledge_loss]
|
|
472
502
|
|
|
473
503
|
|
|
474
504
|
def evaluate(report: dict) -> list:
|
|
@@ -30,10 +30,20 @@ def select_files(repo: str, ignore=(), types_spec: str = None) -> list:
|
|
|
30
30
|
return [f for f in files if lizard.get_reader_for(f) is not None]
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
NAME_CAP = 200 # a deeply nested fixture gives lizard a dotted name of megabytes; nobody reads past this
|
|
34
|
+
LONG_NAME_CAP = 500
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cut(text: str, cap: int) -> str:
|
|
38
|
+
return text if len(text) <= cap else text[:cap - 1] + "…"
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
def csv_row(info, fn) -> list:
|
|
34
|
-
"""The columns `lizard --csv` prints, so the loader does not care which produced the file.
|
|
42
|
+
"""The columns `lizard --csv` prints, so the loader does not care which produced the file. Names
|
|
43
|
+
are cut to what a table can show, so one pathological fixture cannot make the file unreadable."""
|
|
44
|
+
name = _cut(fn.name, NAME_CAP)
|
|
35
45
|
return [fn.nloc, fn.cyclomatic_complexity, fn.token_count, fn.parameter_count, fn.length,
|
|
36
|
-
f"{
|
|
46
|
+
f"{name}@{fn.start_line}-{fn.end_line}@{info.filename}", info.filename, name, _cut(fn.long_name, LONG_NAME_CAP), fn.start_line, fn.end_line]
|
|
37
47
|
|
|
38
48
|
|
|
39
49
|
def write_duplicates(dup: Duplicates, fh) -> None:
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import re
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
_BOT_WORDS = ("dependabot", "renovate", "github-actions", "github actions")
|
|
7
|
+
_BOT_WORDS = ("dependabot", "renovate", "github-actions", "github actions", "copilot", "cursor agent", "cursor-agent", "cursoragent")
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def is_bot(name: str, email: str = "") -> bool:
|
|
@@ -33,13 +33,27 @@ def _plain(name: str) -> str:
|
|
|
33
33
|
return " ".join(name.lower().split())
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _distinctive(token: str) -> bool:
|
|
37
|
+
"""A word that names one person on its own: five letters or more and not a common first name."""
|
|
38
|
+
return len(token) >= 5 and token not in _COMMON_FIRST_NAMES
|
|
39
|
+
|
|
40
|
+
|
|
36
41
|
def same_person(a: dict, b: dict) -> bool:
|
|
37
|
-
"""Same email
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
"""Same email; two shared name tokens; the same name spelled identically (a handle such as KaKa
|
|
43
|
+
under three emails), unless that name is a bare common first name; or a one-word handle that is
|
|
44
|
+
one distinctive word of the other's fuller name (junegunn and Junegunn Choi)."""
|
|
45
|
+
if a["email"].lower() == b["email"].lower():
|
|
46
|
+
return True
|
|
47
|
+
ta, tb = _tokens(a["name"]), _tokens(b["name"])
|
|
48
|
+
if len(ta & tb) >= 2:
|
|
49
|
+
return True
|
|
50
|
+
na, nb = _plain(a["name"]), _plain(b["name"])
|
|
51
|
+
if na and na == nb and na not in _COMMON_FIRST_NAMES:
|
|
40
52
|
return True
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
for handle, full in ((na, tb), (nb, ta)):
|
|
54
|
+
if " " not in handle and handle in full and len(full) >= 2 and _distinctive(handle):
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
43
57
|
|
|
44
58
|
|
|
45
59
|
def merge(identities: list) -> list:
|
|
@@ -40,6 +40,10 @@ _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
42
|
"baz", "admin", "root", "user", "123456", "12345678", "123456789", "abc123", "qwerty", "letmein", "welcome"}
|
|
43
|
+
# A whole value that refers to an environment variable or a template field is where the secret will be
|
|
44
|
+
# read from, not the secret: `@env:AC_PASSWORD`, `${DB_PASSWORD}`, `{{.Env.X}}`, `process.env.X`, `<%= ENV['X'] %>`.
|
|
45
|
+
_ENV_REF = re.compile(r"^(@env:\w+|\$\{[^{}]+\}|\$[A-Za-z_]\w*|\$\([^()]+\)|%[A-Za-z_]\w*%|\{\{.*\}\}|<%=?.*%>"
|
|
46
|
+
r"|process\.env\.\w+|os\.environ(\[[^\]]+\]|\.get\([^()]*\))|ENV\[[^\]]+\])$", re.S)
|
|
43
47
|
_KEY_BLOCK = re.compile(r"-----BEGIN [A-Z ]*KEY-----(.*?)-----END [A-Z ]*KEY-----", re.S)
|
|
44
48
|
_KEY_MATERIAL = 64 # a real body is hundreds of base64 characters; a template has dots or a few x's
|
|
45
49
|
|
|
@@ -57,7 +61,8 @@ def digest(value: str, key: bytes) -> str:
|
|
|
57
61
|
|
|
58
62
|
def is_placeholder(value: str) -> bool:
|
|
59
63
|
value = (value or "").strip()
|
|
60
|
-
if _VERSION.match(value) or value.endswith("...") or value.endswith("…") or _MARKER.match(value) or value.lower() in _EXAMPLE_WORDS
|
|
64
|
+
if (_VERSION.match(value) or value.endswith("...") or value.endswith("…") or _MARKER.match(value) or value.lower() in _EXAMPLE_WORDS
|
|
65
|
+
or _ENV_REF.match(value)):
|
|
61
66
|
return True
|
|
62
67
|
m = _KEY_BLOCK.search(value)
|
|
63
68
|
if m:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import csv
|
|
5
5
|
import io
|
|
6
|
+
import sys
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
@@ -145,13 +146,21 @@ def parse_authors_log(text: str) -> list:
|
|
|
145
146
|
]
|
|
146
147
|
|
|
147
148
|
|
|
149
|
+
NAME_CAP = 200 # a function name a table can show; deeply nested fixtures give lizard dotted names of megabytes
|
|
150
|
+
csv.field_size_limit(min(sys.maxsize, 2**31 - 1)) # an older functions.csv may still carry such a name
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cut(name: str, cap: int = NAME_CAP) -> str:
|
|
154
|
+
return name if len(name) <= cap else name[:cap - 1] + "…"
|
|
155
|
+
|
|
156
|
+
|
|
148
157
|
def parse_functions(text: str) -> list:
|
|
149
158
|
"""lizard --csv rows: nloc, ccn, tokens, params, length, location, file, function, long name, start, end."""
|
|
150
159
|
rows = []
|
|
151
160
|
for r in csv.reader(io.StringIO(text)):
|
|
152
161
|
if len(r) < 11:
|
|
153
162
|
continue
|
|
154
|
-
rows.append({"file": _rel(r[6]), "function": r[7] or "(anonymous)", "ccn": _num(r[1]), "nloc": _num(r[0]), "params": _num(r[3]),
|
|
163
|
+
rows.append({"file": _rel(r[6]), "function": _cut(r[7]) or "(anonymous)", "ccn": _num(r[1]), "nloc": _num(r[0]), "params": _num(r[3]),
|
|
155
164
|
"start": _num(r[9]), "end": _num(r[10])})
|
|
156
165
|
return rows
|
|
157
166
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitmole
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
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
|
|
@@ -305,6 +305,29 @@ class ReleasePlumbing(unittest.TestCase):
|
|
|
305
305
|
self.assertNotIn("package.json", f[0]["detail"])
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
class Dormant(unittest.TestCase):
|
|
309
|
+
def test_a_year_without_commits_is_a_warning_that_dates_the_last_one(self):
|
|
310
|
+
r = report()
|
|
311
|
+
r["meta"].update({"last_date": "2025-06-14", "now": "2026-09-17"})
|
|
312
|
+
f = findings.dormant(r)
|
|
313
|
+
self.assertEqual(f[0]["severity"], "warning")
|
|
314
|
+
self.assertEqual(f[0]["title"], "Dormant repository")
|
|
315
|
+
self.assertIn("No commits since 2025-06-14, 15 months ago.", f[0]["detail"])
|
|
316
|
+
self.assertIn("stopped", f[0]["advice"])
|
|
317
|
+
|
|
318
|
+
def test_a_recent_commit_is_not_dormant(self):
|
|
319
|
+
r = report()
|
|
320
|
+
r["meta"].update({"last_date": "2026-06-14", "now": "2026-09-17"})
|
|
321
|
+
self.assertEqual(findings.dormant(r), [])
|
|
322
|
+
self.assertEqual(findings.dormant(report()), [], "no dates, no finding")
|
|
323
|
+
|
|
324
|
+
def test_stale_files_are_not_reported_for_a_dormant_repository(self):
|
|
325
|
+
age = [{"entity": f"f{i}", "age-months": 15} for i in range(10)]
|
|
326
|
+
r = report(age=age)
|
|
327
|
+
r["meta"].update({"last_date": "2025-06-14", "now": "2026-09-17"})
|
|
328
|
+
self.assertEqual(findings.stale_files(r), [], "every file is untouched because nothing is; the dormancy finding says so")
|
|
329
|
+
|
|
330
|
+
|
|
308
331
|
class StaleFiles(unittest.TestCase):
|
|
309
332
|
def test_info_when_a_third_untouched_for_a_year(self):
|
|
310
333
|
age = [{"entity": f"f{i}", "age-months": 12} for i in range(4)] + [{"entity": "g", "age-months": 0} for _ in range(6)]
|
|
@@ -67,6 +67,17 @@ class FunctionsScript(unittest.TestCase):
|
|
|
67
67
|
self.assertIn('"tracked"', csv, "functions are still measured")
|
|
68
68
|
self.assertIsNone(dup, "no duplicates.txt: the finder did not run, so nothing pretends it did")
|
|
69
69
|
|
|
70
|
+
def test_a_huge_nested_name_is_cut_before_it_is_written(self):
|
|
71
|
+
from types import SimpleNamespace
|
|
72
|
+
name = ".".join("a" for _ in range(100_000))
|
|
73
|
+
fn = SimpleNamespace(nloc=5, cyclomatic_complexity=3, token_count=40, parameter_count=1, length=5, name=name,
|
|
74
|
+
long_name=name + "( )", start_line=1, end_line=5)
|
|
75
|
+
row = functions.csv_row(SimpleNamespace(filename="a.py"), fn)
|
|
76
|
+
self.assertEqual(len(row[7]), functions.NAME_CAP)
|
|
77
|
+
self.assertTrue(row[7].endswith("…"))
|
|
78
|
+
self.assertLessEqual(len(row[8]), functions.LONG_NAME_CAP)
|
|
79
|
+
self.assertLessEqual(len(row[5]), functions.NAME_CAP + len("@1-5@a.py"))
|
|
80
|
+
|
|
70
81
|
def test_csv_matches_lizards_own_layout(self):
|
|
71
82
|
with tempfile.TemporaryDirectory() as d:
|
|
72
83
|
make_repo(d)
|
|
@@ -26,6 +26,14 @@ class Merge(unittest.TestCase):
|
|
|
26
26
|
merged = {m["name"]: m["commits"] for m in identity.merge(ids)}
|
|
27
27
|
self.assertEqual(merged, {"KaKa": 123, "namusyaka": 188, "Li Yu": 8})
|
|
28
28
|
|
|
29
|
+
def test_a_handle_that_is_one_distinctive_word_of_a_fuller_name_is_the_same_person(self):
|
|
30
|
+
ids = [{"name": "Junegunn Choi", "email": "junegunn.c@a.com", "commits": 2924}, {"name": "junegunn", "email": "junegunn@b.com", "commits": 123},
|
|
31
|
+
{"name": "Sam Altman", "email": "sam@a.com", "commits": 5}, {"name": "sam", "email": "sam@b.com", "commits": 2},
|
|
32
|
+
{"name": "Kevin Brown", "email": "kb@a.com", "commits": 76}, {"name": "Kevin", "email": "k@b.com", "commits": 21}]
|
|
33
|
+
merged = {m["name"]: m["commits"] for m in identity.merge(ids)}
|
|
34
|
+
self.assertEqual(merged, {"Junegunn Choi": 3047, "Sam Altman": 5, "sam": 2, "Kevin Brown": 76, "Kevin": 21},
|
|
35
|
+
"a short or common first name is not distinctive enough")
|
|
36
|
+
|
|
29
37
|
def test_a_bare_common_first_name_is_not_enough(self):
|
|
30
38
|
ids = [{"name": "Jean", "email": "jean@a.com", "commits": 24}, {"name": "Jean", "email": "jean@b.com", "commits": 18},
|
|
31
39
|
{"name": "Alex", "email": "alex@a.com", "commits": 3}, {"name": "alex", "email": "alex@b.com", "commits": 2}]
|
|
@@ -76,7 +84,8 @@ class IsBot(unittest.TestCase):
|
|
|
76
84
|
for name, email in [("renovate[bot]", "29139614+renovate[bot]@users.noreply.github.com"),
|
|
77
85
|
("github-actions[bot]", "41898282+github-actions[bot]@users.noreply.github.com"),
|
|
78
86
|
("dependabot[bot]", "support@github.com"), ("Dependabot", "dependabot@example.com"),
|
|
79
|
-
("Renovate Bot", "bot@renovateapp.com"), ("GitHub Actions", "actions@github.com")
|
|
87
|
+
("Renovate Bot", "bot@renovateapp.com"), ("GitHub Actions", "actions@github.com"),
|
|
88
|
+
("Copilot", "198982749+Copilot@users.noreply.github.com"), ("Cursor Agent", "cursoragent@cursor.com")]:
|
|
80
89
|
self.assertTrue(identity.is_bot(name, email), (name, email))
|
|
81
90
|
|
|
82
91
|
def test_people_are_not_bots(self):
|
|
@@ -66,6 +66,14 @@ class Placeholder(unittest.TestCase):
|
|
|
66
66
|
for value in ["hello123", "secret-9f8a7b6c5d4e", "s3cr3t!Passw0rd", "foobarbaz2024"]:
|
|
67
67
|
self.assertFalse(leaks.is_placeholder(value), "a word inside other material is not a placeholder: " + value)
|
|
68
68
|
|
|
69
|
+
def test_references_to_an_environment_variable_are_placeholders(self):
|
|
70
|
+
# fzf's notarisation config: `password = "@env:AC_PASSWORD"`; goreleaser: `{{.Env.MACOS_SIGN_PASSWORD}}`
|
|
71
|
+
for value in ["@env:AC_PASSWORD", "{{.Env.MACOS_SIGN_PASSWORD}}", "${DB_PASSWORD}", "$DB_PASSWORD", "$(cat ~/.secret)", "%APPDATA_KEY%",
|
|
72
|
+
"{{ secrets.API_TOKEN }}", "<%= ENV['KEY'] %>", "process.env.API_KEY", "os.environ['API_KEY']", "os.environ.get('K')", "ENV['SECRET']"]:
|
|
73
|
+
self.assertTrue(leaks.is_placeholder(value), value)
|
|
74
|
+
for value in ["p@ssw0rd!", "AKIA" + "X" * 16, "env-9f8a7b6c5d4e3f2a", "${not closed", "hello$world"]:
|
|
75
|
+
self.assertFalse(leaks.is_placeholder(value), value)
|
|
76
|
+
|
|
69
77
|
def test_anything_else_is_taken_seriously(self):
|
|
70
78
|
# built at runtime: a literal in these shapes would trip secret scanners on this very file
|
|
71
79
|
key_id, long_key = "AKIA" + "X" * 16, "6L" + "x" * 38
|
|
@@ -138,6 +138,19 @@ class ParseFunctions(unittest.TestCase):
|
|
|
138
138
|
def test_empty(self):
|
|
139
139
|
self.assertEqual(load.parse_functions(""), [])
|
|
140
140
|
|
|
141
|
+
def test_a_row_with_a_huge_long_name_does_not_abort_the_report(self):
|
|
142
|
+
# ruff: lizard wrote a long name of several hundred kilobytes for one function, over csv's default field limit
|
|
143
|
+
huge = "f( " + "a, " * 100_000 + ")"
|
|
144
|
+
rows = load.parse_functions(f'5,3,40,1,5,"f@1-5@a.rs","a.rs","f","{huge}",1,5\n')
|
|
145
|
+
self.assertEqual((rows[0]["function"], rows[0]["file"], rows[0]["ccn"]), ("f", "a.rs", 3))
|
|
146
|
+
|
|
147
|
+
def test_a_huge_function_name_is_cut_to_something_a_table_can_show(self):
|
|
148
|
+
# a test fixture of deeply nested functions gives lizard a dotted name of megabytes
|
|
149
|
+
name = ".".join("a" for _ in range(100_000))
|
|
150
|
+
rows = load.parse_functions(f'5,3,40,1,5,"{name}@1-5@a.py","a.py","{name}","{name}( )",1,5\n')
|
|
151
|
+
self.assertEqual(len(rows[0]["function"]), load.NAME_CAP)
|
|
152
|
+
self.assertTrue(rows[0]["function"].endswith("…"))
|
|
153
|
+
|
|
141
154
|
def test_a_nameless_function_is_called_anonymous(self):
|
|
142
155
|
# lizard names Go function literals with an empty string where it names JavaScript's "(anonymous)"
|
|
143
156
|
rows = load.parse_functions('136,47,926,1,270,"@316-585@completions.go","completions.go",""," c * Command",316,585\n')
|
|
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
|
|
File without changes
|
|
File without changes
|