Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ the selected table. Keyboard focus has a visible outline.
- **Auditing filters**: Searchable and filterable tables for repositories, recent pull requests, and commit logs with links directly to GitHub.
- **Arbitrary timeframes**: Audit any historical period using `--weeks <N>` or explicit `--start YYYY-MM-DD` and `--end YYYY-MM-DD` dates.

Repository profile language follows the largest file-churn total in the selected
interval. Without file-derived language, it uses GitHub's primary language.
Missing, null, or empty language metadata displays `Unknown`, including repositories
whose commits change no files.
Repository profile language uses GitHub's primary language for the repository,
independent of file churn in the selected interval. Missing, null, or empty
language metadata displays `Unknown`; recent changes alone do not establish a
repository's implementation language. Lockfile activity remains included in
commit, line-churn, and file-type breakdown totals.

PR cycle-time medians use merged PRs with a known creation-to-merge duration.
Even-sized samples average the middle two values; odd-sized samples use the
Expand Down
12 changes: 12 additions & 0 deletions scripts/generate_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ def main():
build_html_report(analyze_data(organization)), encoding="utf-8"
)

languages = load_report_fixture()
languages["repo_meta"]["hs-buddy"]["primaryLanguage"] = {"name": "TypeScript"}
commit = languages["commits"][0]
commit["files"].append({"path": "uv.lock", "language": "Lockfile", "adds": 10000, "dels": 5000})
commit["additions"] += 10000
commit["deletions"] += 5000
commit["net_lines"] += 5000
languages["commits"].append(dict(commit, repo="unknown-language", hash="e" * 40))
(output / "report-languages.html").write_text(
build_html_report(analyze_data(languages)), encoding="utf-8"
)

# Exercise the directory with repeated editions, long names and actual local targets.
site = output / "landing"
site.mkdir(exist_ok=True)
Expand Down
9 changes: 2 additions & 7 deletions src/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,8 @@ def analyze_data(data):
r_dels = sum(c["deletions"] for c in r_commits)
r_net = r_adds - r_dels

# Languages in this repo
r_langs = Counter()
for c in r_commits:
for f in c.get("files", []):
r_langs[f["language"]] += f["adds"] + f["dels"]
metadata_language = (meta.get("primaryLanguage") or {}).get("name") or "Unknown"
primary_lang = r_langs.most_common(1)[0][0] if r_langs else metadata_language
# Repository language describes the project, not its recent file churn.
primary_lang = (meta.get("primaryLanguage") or {}).get("name") or "Unknown"

r_active_days = len(set(c["date_str"] for c in r_commits))
first_commit = min((c["author_date"] for c in r_commits), default=None)
Expand Down
14 changes: 14 additions & 0 deletions tests/browser/report.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@ test('organization repository and commit links retain canonical owner alongside
await expect(page.locator('#pane-commits a[href^="https://github.com/HemSoft/retained/commit/"]')).toHaveCount(1);
await expect(page.locator('#pane-commits a[href^="https://github.com/HemSoft/hs-buddy/commit/"]')).toHaveCount(0);
});

test('repository languages use metadata despite dominant lockfile churn', async ({ page }) => {
await openReport(page, { reportPath: path.join(root, 'test-results/report-languages.html') });
const known = page.locator('#table-repos tbody tr').filter({ has: page.getByRole('link', { name: 'hs-buddy', exact: true }) });
await expect(known.locator('td').nth(2)).toHaveText('TypeScript');
await expect(known.locator('td').nth(3)).toHaveText('2');
await expect(known.locator('td').nth(5)).toHaveText('+10,110');
await expect(known.locator('td').nth(6)).toHaveText('-5,025');
const unknown = page.locator('#table-repos tbody tr').filter({ has: page.getByRole('link', { name: 'unknown-language', exact: true }) });
await expect(unknown.locator('td').nth(2)).toHaveText('Unknown');
await expect(page.locator('#table-repos')).not.toContainText('Lockfile');
await page.locator('#pane-repos').scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(root, 'test-results/repository-languages.png') });
});
45 changes: 43 additions & 2 deletions tests/test_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,55 @@ def test_empty_commit_profiles_handle_missing_null_and_empty_language(self):
self.assertEqual(analysis["repo_profiles"][0]["commits"], 1)
self.assertIn("Unknown", build_html_report(analysis))

def test_profile_language_uses_churn_then_metadata(self):
def test_profile_language_uses_repository_metadata(self):
data = copy.deepcopy(self.sample_data)
data["repo_meta"]["hs-buddy"]["primaryLanguage"] = {"name": "Python"}
self.assertEqual(analyze_data(data)["repo_profiles"][0]["primary_language"], "TypeScript")
self.assertEqual(analyze_data(data)["repo_profiles"][0]["primary_language"], "Python")
for commit in data["commits"]:
commit.update(files=[], additions=0, deletions=0, net_lines=0)
self.assertEqual(analyze_data(data)["repo_profiles"][0]["primary_language"], "Python")

def test_lockfile_churn_does_not_replace_repository_language_or_activity(self):
data = copy.deepcopy(self.sample_data)
data["repo_meta"]["hs-buddy"]["primaryLanguage"] = {"name": "TypeScript"}
commit = data["commits"][0]
commit["files"].append(
{"path": "uv.lock", "language": "Lockfile", "adds": 10000, "dels": 5000}
)
commit["additions"] += 10000
commit["deletions"] += 5000
commit["net_lines"] += 5000
analysis = analyze_data(data)
profile = analysis["repo_profiles"][0]
self.assertEqual(profile["primary_language"], "TypeScript")
self.assertEqual(profile["commits"], 2)
self.assertEqual(profile["additions"], 10110)
self.assertEqual(profile["deletions"], 5025)
self.assertEqual(analysis["kpis"]["total_additions"], 10110)
self.assertEqual(analysis["kpis"]["total_deletions"], 5025)
lockfile = next(
row for row in analysis["language_breakdown"] if row["language"] == "Lockfile"
)
self.assertEqual(lockfile["additions"], 10000)
self.assertEqual(lockfile["deletions"], 5000)

def test_missing_repository_language_does_not_infer_from_interval_churn(self):
for metadata in (
{},
{"primaryLanguage": None},
{"primaryLanguage": {}},
{"primaryLanguage": {"name": ""}},
):
with self.subTest(metadata=metadata):
data = copy.deepcopy(self.sample_data)
data["repo_meta"]["hs-buddy"] = metadata
data["commits"][0]["files"].append(
{"path": "uv.lock", "language": "Lockfile", "adds": 10000, "dels": 0}
)
self.assertEqual(
analyze_data(data)["repo_profiles"][0]["primary_language"], "Unknown"
)

def test_median_cycle_time_for_even_odd_empty_and_single_samples(self):
cases = [
([0.5, 3.5], 2.0),
Expand Down
Loading