Skip to content

Commit a35dae6

Browse files
authored
feat(cli, github, prompt): add GitHub directory analysis (#217)
* feat(cli): add GitHub directory analysis mode * feat(github): add directory contents fetch and tighten GitHub API error handling * feat(prompt): add GitHub directory prompt builders and signal formatting * chore: bump version to v0.25.0 and update rich dependeicies to 15.0.0
2 parents 93c1e38 + b628680 commit a35dae6

8 files changed

Lines changed: 495 additions & 26 deletions

File tree

_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "0.24.1"
1+
VERSION = "0.25.0"

explain_this_repo/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "0.24.1"
1+
VERSION = "0.25.0"

explain_this_repo/cli.py

Lines changed: 154 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import argparse
24
import os
35
import platform
@@ -13,13 +15,17 @@
1315
from explain_this_repo.file_reader import read_local_file
1416
from explain_this_repo.generate import generate_explanation
1517
from explain_this_repo.github import (
18+
fetch_directory_contents,
1619
fetch_file_result,
1720
fetch_languages,
1821
fetch_readme,
1922
fetch_repo,
2023
)
2124
from explain_this_repo.local_reader import read_local_repo_signal_files
2225
from explain_this_repo.prompt import (
26+
build_directory_prompt,
27+
build_directory_quick_prompt,
28+
build_directory_simple_prompt,
2329
build_file_prompt,
2430
build_file_quick_prompt,
2531
build_file_simple_prompt,
@@ -313,7 +319,7 @@ def _classify_target(target: str) -> str:
313319
if os.path.isdir(target):
314320
return "directory"
315321
if _looks_like_github_file_target(target):
316-
return "github_file"
322+
return "github_directory"
317323
return "github"
318324

319325

@@ -324,6 +330,43 @@ def _extract_file_signals(read_result) -> dict:
324330
"size_bytes": read_result.size_bytes,
325331
}
326332

333+
334+
def _extract_directory_signals(contents: list[dict]) -> dict:
335+
files: list[str] = []
336+
directories: list[str] = []
337+
extensions: dict[str, int] = {}
338+
339+
for entry in contents:
340+
if not isinstance(entry, dict):
341+
continue
342+
343+
entry_type = str(entry.get("type") or "").lower()
344+
name = str(entry.get("name") or entry.get("path") or "").strip()
345+
346+
if not name:
347+
continue
348+
349+
if entry_type == "dir":
350+
directories.append(name)
351+
continue
352+
353+
files.append(name)
354+
extension = os.path.splitext(name)[1].lower().lstrip(".") or "no_extension"
355+
extensions[extension] = extensions.get(extension, 0) + 1
356+
357+
files.sort(key=str.lower)
358+
directories.sort(key=str.lower)
359+
extensions = dict(sorted(extensions.items(), key=lambda item: (-item[1], item[0])))
360+
361+
return {
362+
"files": files,
363+
"directories": directories,
364+
"file_count": len(files),
365+
"dir_count": len(directories),
366+
"extensions": extensions,
367+
}
368+
369+
327370
def _handle_file_mode(args, llm: str | None) -> None:
328371
if args.stack:
329372
print("error: --stack is not supported for file targets")
@@ -399,22 +442,29 @@ def _handle_file_mode(args, llm: str | None) -> None:
399442
print(f"Open {args.output} to read it.")
400443

401444

402-
def _handle_github_file_mode(args, llm: str | None) -> None:
445+
def _handle_github_file_mode(
446+
args,
447+
llm: str | None,
448+
owner: str | None = None,
449+
repo: str | None = None,
450+
file_path: str | None = None,
451+
) -> None:
403452
if args.stack:
404453
print("error: --stack is not supported for GitHub file targets")
405454
raise SystemExit(1)
406455

407-
try:
408-
owner, repo, file_path = resolve_github_file_target(args.repository)
409-
except ValueError as e:
410-
print(f"error: {str(e)}")
411-
raise SystemExit(1)
456+
if owner is None or repo is None or file_path is None:
457+
try:
458+
owner, repo, file_path = resolve_github_file_target(args.repository)
459+
except ValueError as e:
460+
print(f"error: {e}")
461+
raise SystemExit(1)
412462

413463
try:
414464
with console.status(f"Fetching {owner}/{repo}/{file_path}...", spinner="dots"):
415465
read_result = fetch_file_result(owner, repo, file_path)
416466
except Exception as e:
417-
print(f"error: {str(e)}")
467+
print(f"error: {e}")
418468
raise SystemExit(1)
419469

420470
display_path = f"{owner}/{repo}/{read_result.path}"
@@ -473,6 +523,94 @@ def _handle_github_file_mode(args, llm: str | None) -> None:
473523
print(f"Open {args.output} to read it.")
474524

475525

526+
def _handle_github_directory_mode(
527+
args,
528+
llm: str | None,
529+
owner: str | None = None,
530+
repo: str | None = None,
531+
directory_path: str | None = None,
532+
) -> None:
533+
if args.stack:
534+
print("error: --stack is not supported for GitHub directory targets")
535+
raise SystemExit(1)
536+
537+
if owner is None or repo is None or directory_path is None:
538+
try:
539+
owner, repo, directory_path = resolve_github_file_target(args.repository)
540+
except ValueError as e:
541+
print(f"error: {e}")
542+
raise SystemExit(1)
543+
544+
try:
545+
with console.status(f"Fetching {owner}/{repo}/{directory_path}...", spinner="dots"):
546+
contents = fetch_directory_contents(owner, repo, directory_path)
547+
except Exception as e:
548+
message = str(e).strip()
549+
lowered = message.lower()
550+
551+
if "file" in lowered and "directory" in lowered:
552+
_handle_github_file_mode(
553+
args,
554+
llm,
555+
owner=owner,
556+
repo=repo,
557+
file_path=directory_path,
558+
)
559+
return
560+
561+
print(f"error: {message}")
562+
raise SystemExit(1)
563+
564+
display_path = f"{owner}/{repo}/{directory_path}"
565+
print(f"Analyzing GitHub directory: {display_path}")
566+
567+
signals = _extract_directory_signals(contents)
568+
569+
if args.quick:
570+
prompt = build_directory_quick_prompt(
571+
directory_path=display_path,
572+
signals=signals,
573+
)
574+
575+
with console.status("Generating explanation...", spinner="dots"):
576+
output = generate_with_exit(prompt, llm=llm)
577+
578+
print("Quick summary 🎉")
579+
print(output.strip())
580+
return
581+
582+
if args.simple:
583+
prompt = build_directory_simple_prompt(
584+
directory_path=display_path,
585+
signals=signals,
586+
)
587+
588+
with console.status("Generating explanation...", spinner="dots"):
589+
output = generate_with_exit(prompt, llm=llm)
590+
591+
print("Simple summary 🎉")
592+
print(output.strip())
593+
return
594+
595+
prompt = build_directory_prompt(
596+
directory_path=display_path,
597+
signals=signals,
598+
detailed=args.detailed,
599+
)
600+
601+
with console.status("Generating explanation...", spinner="dots"):
602+
output = generate_with_exit(prompt, llm=llm)
603+
604+
print(f"Writing {args.output}...")
605+
write_output(output, args.output)
606+
607+
word_count = len(output.split())
608+
print(f"{args.output} generated successfully 🎉")
609+
print(f"Words: {word_count}")
610+
print(f"Location: {os.path.abspath(args.output)}")
611+
print(f"Open {args.output} to read it.")
612+
613+
476614
def _handle_directory_mode(args, llm: str | None) -> None:
477615
local_path = os.path.abspath(args.repository)
478616

@@ -662,10 +800,10 @@ def main():
662800
" explainthisrepo owner/repo --quick\n"
663801
" explainthisrepo owner/repo --simple\n"
664802
" explainthisrepo owner/repo --stack\n"
665-
" explainthisrepo owner/repo/path/to/file.py\n"
666-
" explainthisrepo owner/repo/path/to/file.py --quick\n"
667-
" explainthisrepo owner/repo/path/to/file.py --simple\n"
668-
" explainthisrepo owner/repo/path/to/file.py --detailed\n"
803+
" explainthisrepo owner/repo/packages/react-dom\n"
804+
" explainthisrepo owner/repo/packages/react-dom --quick\n"
805+
" explainthisrepo owner/repo/packages/react-dom --simple\n"
806+
" explainthisrepo owner/repo/packages/react-dom --detailed\n"
669807
" explainthisrepo init\n"
670808
" explainthisrepo owner/repo --llm gemini\n"
671809
" explainthisrepo owner/repo --llm openai\n"
@@ -739,7 +877,7 @@ def main():
739877
parser.add_argument(
740878
"repository",
741879
nargs="?",
742-
help="GitHub repository (owner/repo or URL), local directory, GitHub file, or local file",
880+
help="GitHub repository (owner/repo or URL), GitHub file or directory path, local directory, or local file",
743881
)
744882

745883
mode_group = parser.add_mutually_exclusive_group()
@@ -805,8 +943,8 @@ def main():
805943
_handle_file_mode(args, llm)
806944
elif mode == "directory":
807945
_handle_directory_mode(args, llm)
808-
elif mode == "github_file":
809-
_handle_github_file_mode(args, llm)
946+
elif mode == "github_directory":
947+
_handle_github_directory_mode(args, llm)
810948
else:
811949
_handle_github_mode(args, llm)
812950

@@ -820,4 +958,4 @@ def _run():
820958

821959

822960
if __name__ == "__main__":
823-
_run()
961+
_run()

0 commit comments

Comments
 (0)