chronicle
¶
Full name: tenets.cli.commands.chronicle
chronicle¶
Chronicle command implementation.
This command provides git history analysis and visualization of code evolution over time, including contribution patterns and change dynamics.
Classes¶
Functions¶
run¶
Python
run(path: str = typer.Argument('.', help='Repository directory'), since: Optional[str] = typer.Option(None, '--since', '-s', help='Start date (YYYY-MM-DD or relative like "3 months ago")'), until: Optional[str] = typer.Option(None, '--until', '-u', help='End date (YYYY-MM-DD or relative like "today")'), output: Optional[str] = typer.Option(None, '--output', '-o', help='Output file for report'), format: str = typer.Option('terminal', '--format', '-f', help='Output format', case_sensitive=False), branch: str = typer.Option('main', '--branch', '-b', help='Git branch to analyze'), authors: Optional[List[str]] = typer.Option(None, '--authors', '-a', help='Filter by specific authors'), show_merges: bool = typer.Option(False, '--show-merges', help='Include merge commits'), show_contributors: bool = typer.Option(False, '--show-contributors', help='Show contributor analysis'), show_patterns: bool = typer.Option(False, '--show-patterns', help='Show change patterns'), limit: Optional[int] = typer.Option(None, '--limit', '-l', help='Limit number of commits to analyze'))
Chronicle the evolution of your codebase.
This runs as the app callback so tests can invoke chronicle
directly.
Source code in tenets/cli/commands/chronicle.py
Python
@chronicle.callback()
def run(
path: str = typer.Argument(".", help="Repository directory"),
since: Optional[str] = typer.Option(
None, "--since", "-s", help='Start date (YYYY-MM-DD or relative like "3 months ago")'
),
until: Optional[str] = typer.Option(
None, "--until", "-u", help='End date (YYYY-MM-DD or relative like "today")'
),
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file for report"),
format: str = typer.Option(
"terminal",
"--format",
"-f",
help="Output format",
case_sensitive=False,
),
branch: str = typer.Option("main", "--branch", "-b", help="Git branch to analyze"),
authors: Optional[List[str]] = typer.Option(
None, "--authors", "-a", help="Filter by specific authors"
),
show_merges: bool = typer.Option(False, "--show-merges", help="Include merge commits"),
show_contributors: bool = typer.Option(
False, "--show-contributors", help="Show contributor analysis"
),
show_patterns: bool = typer.Option(False, "--show-patterns", help="Show change patterns"),
limit: Optional[int] = typer.Option(
None, "--limit", "-l", help="Limit number of commits to analyze"
),
):
"""Chronicle the evolution of your codebase.
This runs as the app callback so tests can invoke `chronicle` directly.
"""
logger = get_logger(__name__)
config = None # tests invoke this in isolation without Typer app context
# Initialize timer
is_quiet = format.lower() == "json" and not output
timer = CommandTimer(quiet=is_quiet)
timer.start("Initializing git chronicle...")
# Initialize path; allow non-existent for most tests except explicit invalid paths
target_path = Path(path).resolve()
norm_path = str(path).replace("\\", "/").strip()
if norm_path.startswith("nonexistent/") or norm_path == "nonexistent":
click.echo(f"Error: Path does not exist: {target_path}")
raise typer.Exit(1)
logger.info(f"Chronicling repository at: {target_path}")
# Initialize chronicle builder
chronicle_builder = ChronicleBuilder(config)
git_analyzer = GitAnalyzer(normalize_path(target_path))
# Parse date range
date_range = _parse_date_range(since, until)
# Build chronicle options
chronicle_options = {
"branch": branch,
"since": date_range["since"],
"until": date_range["until"],
"authors": list(authors) if authors else None,
"include_merges": show_merges,
"limit": limit,
}
try:
# Build chronicle
logger.info("Building repository chronicle...")
# Pass the resolved path string to help tests inspect call arguments reliably
chronicle_data = chronicle_builder.build_chronicle(
normalize_path(target_path), **chronicle_options
)
# Add contributor analysis if requested
if show_contributors:
logger.info("Analyzing contributors...")
chronicle_data["contributors"] = git_analyzer.analyze_contributors(
since=date_range["since"], until=date_range["until"]
)
# Add pattern analysis if requested
if show_patterns:
logger.info("Analyzing change patterns...")
chronicle_data["patterns"] = _analyze_patterns(git_analyzer, date_range)
# Stop timer
timing_result = timer.stop("Chronicle analysis complete")
chronicle_data["timing"] = {
"duration": timing_result.duration,
"formatted_duration": timing_result.formatted_duration,
"start_time": timing_result.start_datetime.isoformat(),
"end_time": timing_result.end_datetime.isoformat(),
}
# Display or save results
if format == "terminal":
# Simple heading for tests before any rich output
click.echo("Repository Chronicle")
_display_terminal_chronicle(chronicle_data, show_contributors, show_patterns)
# Summary
_print_chronicle_summary(chronicle_data)
# Show timing
if not is_quiet:
click.echo(f"\n⏱ Completed in {timing_result.formatted_duration}")
elif format == "json":
_output_json_chronicle(chronicle_data, output)
return
else:
_generate_chronicle_report(chronicle_data, format, output, config)
# Do not print summary for non-terminal formats
except Exception as e:
# Stop timer on error
if timer.start_time and not timer.end_time:
timing_result = timer.stop("Chronicle failed")
if not is_quiet:
click.echo(f"⚠ Failed after {timing_result.formatted_duration}")
logger.error(f"Chronicle generation failed: {e}")
click.echo(str(e))
raise typer.Exit(1)