Skip to content

blame

Full name: tenets.core.git.blame

blame

Git blame analysis module.

This module provides functionality for analyzing line-by-line authorship of files using git blame. It helps understand who wrote what code, when changes were made, and how code ownership is distributed within files.

The blame analyzer provides detailed insights into code authorship patterns, helping identify knowledge owners and understanding code evolution.

Classes

BlameLinedataclass

Python
BlameLine(
    line_number: int,
    content: str,
    author: str,
    author_email: str,
    commit_sha: str,
    commit_date: datetime,
    commit_message: str,
    is_original: bool = False,
    age_days: int = 0,
    previous_authors: List[str] = list(),
)

Information for a single line from git blame.

Represents authorship information for a specific line of code, including who wrote it, when, and in which commit.

ATTRIBUTEDESCRIPTION
line_number

Line number in file

TYPE:int

content

Content of the line

TYPE:str

author

Author name

TYPE:str

author_email

Author email

TYPE:str

commit_sha

Commit SHA that introduced this line

TYPE:str

commit_date

Date when line was introduced

TYPE:datetime

commit_message

Commit message (first line)

TYPE:str

is_original

Whether this is from the original commit

TYPE:bool

age_days

Age of the line in days

TYPE:int

previous_authors

List of previous authors if line was modified

TYPE:List[str]

Attributes
is_recentproperty
Python
is_recent: bool

Check if line was recently modified.

RETURNSDESCRIPTION
bool

True if modified within last 30 days

TYPE:bool

is_oldproperty
Python
is_old: bool

Check if line is old.

RETURNSDESCRIPTION
bool

True if older than 180 days

TYPE:bool

is_documentationproperty
Python
is_documentation: bool

Check if line appears to be documentation.

RETURNSDESCRIPTION
bool

True if line looks like documentation

TYPE:bool

is_emptyproperty
Python
is_empty: bool

Check if line is empty or whitespace only.

RETURNSDESCRIPTION
bool

True if empty or whitespace

TYPE:bool

FileBlamedataclass

Python
FileBlame(
    file_path: str,
    total_lines: int = 0,
    blame_lines: List[BlameLine] = list(),
    authors: Set[str] = set(),
    author_stats: Dict[str, Dict[str, Any]] = dict(),
    commit_shas: Set[str] = set(),
    oldest_line: Optional[BlameLine] = None,
    newest_line: Optional[BlameLine] = None,
    age_distribution: Dict[str, int] = dict(),
    ownership_map: Dict[str, List[Tuple[int, int]]] = dict(),
    hot_spots: List[Tuple[int, int]] = list(),
)

Blame information for an entire file.

Aggregates line-by-line blame information to provide file-level authorship insights and ownership patterns.

ATTRIBUTEDESCRIPTION
file_path

Path to the file

TYPE:str

total_lines

Total number of lines

TYPE:int

blame_lines

List of blame information per line

TYPE:List[BlameLine]

authors

Set of unique authors

TYPE:Set[str]

author_stats

Statistics per author

TYPE:Dict[str, Dict[str, Any]]

commit_shas

Set of unique commits

TYPE:Set[str]

oldest_line

Oldest line in file

TYPE:Optional[BlameLine]

newest_line

Newest line in file

TYPE:Optional[BlameLine]

age_distribution

Distribution of line ages

TYPE:Dict[str, int]

ownership_map

Line ranges owned by each author

TYPE:Dict[str, List[Tuple[int, int]]]

hot_spots

Lines that changed frequently

TYPE:List[Tuple[int, int]]

Attributes
primary_authorproperty
Python
primary_author: Optional[str]

Get the primary author of the file.

RETURNSDESCRIPTION
Optional[str]

Optional[str]: Author with most lines or None

author_diversityproperty
Python
author_diversity: float

Calculate author diversity score.

Higher scores indicate more distributed authorship.

RETURNSDESCRIPTION
float

Diversity score (0-1)

TYPE:float

average_age_daysproperty
Python
average_age_days: float

Calculate average age of lines in days.

RETURNSDESCRIPTION
float

Average age in days

TYPE:float

freshness_scoreproperty
Python
freshness_score: float

Calculate code freshness score.

Higher scores indicate more recently modified code.

RETURNSDESCRIPTION
float

Freshness score (0-100)

TYPE:float

BlameReportdataclass

Python
BlameReport(
    files_analyzed: int = 0,
    total_lines: int = 0,
    total_authors: int = 0,
    file_blames: Dict[str, FileBlame] = dict(),
    author_summary: Dict[str, Dict[str, Any]] = dict(),
    ownership_distribution: Dict[str, float] = dict(),
    collaboration_matrix: Dict[Tuple[str, str], int] = dict(),
    knowledge_map: Dict[str, Set[str]] = dict(),
    recommendations: List[str] = list(),
    hot_files: List[Dict[str, Any]] = list(),
    single_author_files: List[str] = list(),
    abandoned_code: Dict[str, int] = dict(),
    _bus_factor_override: Optional[int] = None,
    _collab_score_override: Optional[float] = None,
)

Comprehensive blame analysis report.

Provides detailed authorship analysis across multiple files, identifying ownership patterns, knowledge distribution, and collaboration insights.

ATTRIBUTEDESCRIPTION
files_analyzed

Number of files analyzed

TYPE:int

total_lines

Total lines analyzed

TYPE:int

total_authors

Total unique authors

TYPE:int

file_blames

Blame data for each file

TYPE:Dict[str, FileBlame]

author_summary

Summary statistics per author

TYPE:Dict[str, Dict[str, Any]]

ownership_distribution

How ownership is distributed

TYPE:Dict[str, float]

collaboration_matrix

Who modified whose code

TYPE:Dict[Tuple[str, str], int]

knowledge_map

Knowledge areas per author

TYPE:Dict[str, Set[str]]

recommendations

Actionable recommendations

TYPE:List[str]

hot_files

Files with most contributors

TYPE:List[Dict[str, Any]]

single_author_files

Files with only one author

TYPE:List[str]

abandoned_code

Code from inactive authors

TYPE:Dict[str, int]

Attributes
bus_factorpropertywritable
Python
bus_factor: int

Calculate bus factor based on blame data.

RETURNSDESCRIPTION
int

Bus factor (number of critical authors)

TYPE:int

collaboration_scorepropertywritable
Python
collaboration_score: float

Calculate collaboration score.

Higher scores indicate more collaborative development.

RETURNSDESCRIPTION
float

Collaboration score (0-100)

TYPE:float

Methods:
to_dict
Python
to_dict() -> Dict[str, Any]

Convert report to dictionary.

RETURNSDESCRIPTION
Dict[str, Any]

Dict[str, Any]: Dictionary representation

Source code in tenets/core/git/blame.py
Python
def to_dict(self) -> Dict[str, Any]:
    """Convert report to dictionary.

    Returns:
        Dict[str, Any]: Dictionary representation
    """
    return {
        "summary": {
            "files_analyzed": self.files_analyzed,
            "total_lines": self.total_lines,
            "total_authors": self.total_authors,
        },
        "top_authors": sorted(
            self.author_summary.items(),
            key=lambda x: x[1].get("total_lines", 0),
            reverse=True,
        )[:10],
        "ownership_distribution": self.ownership_distribution,
        "hot_files": self.hot_files[:10],
        "single_author_files": len(self.single_author_files),
        "abandoned_lines": sum(self.abandoned_code.values()),
        "recommendations": self.recommendations,
    }

BlameAnalyzer

Python
BlameAnalyzer(config: TenetsConfig)

Analyzer for git blame operations.

Provides line-by-line authorship analysis using git blame, helping understand code ownership and evolution patterns.

ATTRIBUTEDESCRIPTION
config

Configuration object

logger

Logger instance

_blame_cache

Cache for blame results

TYPE:Dict[str, FileBlame]

Initialize blame analyzer.

PARAMETERDESCRIPTION
config

TenetsConfig instance

TYPE:TenetsConfig

Source code in tenets/core/git/blame.py
Python
def __init__(self, config: TenetsConfig):
    """Initialize blame analyzer.

    Args:
        config: TenetsConfig instance
    """
    self.config = config
    self.logger = get_logger(__name__)
    self._blame_cache: Dict[str, FileBlame] = {}
Methods:
analyze_file
Python
analyze_file(
    repo_path: Path,
    file_path: str,
    ignore_whitespace: bool = True,
    follow_renames: bool = True,
) -> FileBlame

Analyze blame for a single file.

Performs git blame analysis on a file to understand line-by-line authorship.

PARAMETERDESCRIPTION
repo_path

Path to git repository

TYPE:Path

file_path

Path to file relative to repo root

TYPE:str

ignore_whitespace

Ignore whitespace changes

TYPE:boolDEFAULT:True

follow_renames

Follow file renames

TYPE:boolDEFAULT:True

RETURNSDESCRIPTION
FileBlame

Blame analysis for the file

TYPE:FileBlame

Example

analyzer = BlameAnalyzer(config) blame = analyzer.analyze_file(Path("."), "src/main.py") print(f"Primary author: {blame.primary_author}")

Source code in tenets/core/git/blame.py
Python
def analyze_file(
    self,
    repo_path: Path,
    file_path: str,
    ignore_whitespace: bool = True,
    follow_renames: bool = True,
) -> FileBlame:
    """Analyze blame for a single file.

    Performs git blame analysis on a file to understand
    line-by-line authorship.

    Args:
        repo_path: Path to git repository
        file_path: Path to file relative to repo root
        ignore_whitespace: Ignore whitespace changes
        follow_renames: Follow file renames

    Returns:
        FileBlame: Blame analysis for the file

    Example:
        >>> analyzer = BlameAnalyzer(config)
        >>> blame = analyzer.analyze_file(Path("."), "src/main.py")
        >>> print(f"Primary author: {blame.primary_author}")
    """
    import subprocess

    self.logger.debug(f"Analyzing blame for {file_path}")

    # Check cache
    cache_key = f"{repo_path}/{file_path}"
    if cache_key in self._blame_cache:
        return self._blame_cache[cache_key]

    file_blame = FileBlame(file_path=file_path)

    # Build git blame command
    cmd = ["git", "blame", "--line-porcelain"]

    if ignore_whitespace:
        cmd.append("-w")

    if follow_renames:
        cmd.append("-C")

    cmd.append(file_path)

    try:
        # Run git blame
        result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True, check=True)

        # Parse blame output
        self._parse_blame_output(result.stdout, file_blame)

        # Calculate statistics
        self._calculate_file_stats(file_blame)

        # Cache result
        self._blame_cache[cache_key] = file_blame

    except subprocess.CalledProcessError as e:
        self.logger.error(f"Git blame failed for {file_path}: {e}")
    except Exception as e:
        self.logger.error(f"Error analyzing blame for {file_path}: {e}")

    return file_blame
analyze_directory
Python
analyze_directory(
    repo_path: Path,
    directory: str = ".",
    file_pattern: str = "*",
    recursive: bool = True,
    max_files: int = 100,
) -> BlameReport

Analyze blame for all files in a directory.

Performs comprehensive blame analysis across multiple files to understand ownership patterns.

PARAMETERDESCRIPTION
repo_path

Path to git repository

TYPE:Path

directory

Directory to analyze

TYPE:strDEFAULT:'.'

file_pattern

File pattern to match

TYPE:strDEFAULT:'*'

recursive

Whether to recurse into subdirectories

TYPE:boolDEFAULT:True

max_files

Maximum files to analyze

TYPE:intDEFAULT:100

RETURNSDESCRIPTION
BlameReport

Comprehensive blame analysis

TYPE:BlameReport

Example

analyzer = BlameAnalyzer(config) report = analyzer.analyze_directory( ... Path("."), ... directory="src", ... file_pattern="*.py" ... ) print(f"Bus factor: {report.bus_factor}")

Source code in tenets/core/git/blame.py
Python
def analyze_directory(
    self,
    repo_path: Path,
    directory: str = ".",
    file_pattern: str = "*",
    recursive: bool = True,
    max_files: int = 100,
) -> BlameReport:
    """Analyze blame for all files in a directory.

    Performs comprehensive blame analysis across multiple files
    to understand ownership patterns.

    Args:
        repo_path: Path to git repository
        directory: Directory to analyze
        file_pattern: File pattern to match
        recursive: Whether to recurse into subdirectories
        max_files: Maximum files to analyze

    Returns:
        BlameReport: Comprehensive blame analysis

    Example:
        >>> analyzer = BlameAnalyzer(config)
        >>> report = analyzer.analyze_directory(
        ...     Path("."),
        ...     directory="src",
        ...     file_pattern="*.py"
        ... )
        >>> print(f"Bus factor: {report.bus_factor}")
    """
    from pathlib import Path as PathLib

    self.logger.debug(f"Analyzing blame for directory {directory}")

    report = BlameReport()

    # Get list of files
    if recursive:
        pattern = f"**/{file_pattern}"
    else:
        pattern = file_pattern

    target_dir = PathLib(repo_path) / directory
    files = list(target_dir.glob(pattern))

    # Filter to only files (not directories)
    files = [f for f in files if f.is_file()]

    # Limit number of files
    if len(files) > max_files:
        self.logger.info(f"Limiting analysis to {max_files} files")
        files = files[:max_files]

    # Analyze each file
    for file_path in files:
        # Get relative path
        try:
            rel_path = file_path.relative_to(repo_path)
        except ValueError:
            continue

        # Skip binary files and common non-source files
        if self._should_skip_file(str(rel_path)):
            continue

        # Analyze file
        file_blame = self.analyze_file(repo_path, str(rel_path))

        if file_blame.total_lines > 0:
            report.file_blames[str(rel_path)] = file_blame
            report.files_analyzed += 1

    # Calculate report statistics
    self._calculate_report_stats(report)

    # Generate recommendations
    report.recommendations = self._generate_recommendations(report)

    self.logger.debug(
        f"Blame analysis complete: {report.files_analyzed} files, "
        f"{report.total_authors} authors"
    )

    return report
get_line_history
Python
get_line_history(
    repo_path: Path, file_path: str, line_number: int, max_depth: int = 10
) -> List[Dict[str, Any]]

Get history of changes for a specific line.

Traces the evolution of a specific line through git history.

PARAMETERDESCRIPTION
repo_path

Path to git repository

TYPE:Path

file_path

Path to file

TYPE:str

line_number

Line number to trace

TYPE:int

max_depth

Maximum history depth to retrieve

TYPE:intDEFAULT:10

RETURNSDESCRIPTION
List[Dict[str, Any]]

List[Dict[str, Any]]: History of line changes

Example

analyzer = BlameAnalyzer(config) history = analyzer.get_line_history( ... Path("."), ... "src/main.py", ... 42 ... ) for change in history: ... print(f"{change['date']}: {change['author']}")

Source code in tenets/core/git/blame.py
Python
def get_line_history(
    self, repo_path: Path, file_path: str, line_number: int, max_depth: int = 10
) -> List[Dict[str, Any]]:
    """Get history of changes for a specific line.

    Traces the evolution of a specific line through git history.

    Args:
        repo_path: Path to git repository
        file_path: Path to file
        line_number: Line number to trace
        max_depth: Maximum history depth to retrieve

    Returns:
        List[Dict[str, Any]]: History of line changes

    Example:
        >>> analyzer = BlameAnalyzer(config)
        >>> history = analyzer.get_line_history(
        ...     Path("."),
        ...     "src/main.py",
        ...     42
        ... )
        >>> for change in history:
        ...     print(f"{change['date']}: {change['author']}")
    """
    import subprocess

    history = []
    current_line = line_number
    current_file = file_path

    for depth in range(max_depth):
        try:
            # Get blame for current line
            cmd = [
                "git",
                "blame",
                "-L",
                f"{current_line},{current_line}",
                "--line-porcelain",
                current_file,
            ]

            result = subprocess.run(
                cmd, cwd=repo_path, capture_output=True, text=True, check=True
            )

            # Parse blame output
            blame_data = self._parse_single_blame(result.stdout)

            if not blame_data:
                break

            history.append(blame_data)

            # Get previous version
            if blame_data["commit"] == "0000000000000000000000000000000000000000":
                break  # Uncommitted changes

            # Find line in parent commit
            parent_cmd = ["git", "show", f"{blame_data['commit']}^:{current_file}"]

            try:
                subprocess.run(
                    parent_cmd, cwd=repo_path, capture_output=True, text=True, check=True
                )
                # Continue with parent
                # This is simplified - real implementation would track line movement
            except subprocess.CalledProcessError:
                break  # File didn't exist in parent

        except subprocess.CalledProcessError:
            break
        except Exception as e:
            self.logger.error(f"Error getting line history: {e}")
            break

    return history

Functions:

analyze_blame

Python
analyze_blame(
    repo_path: Path,
    target: str = ".",
    config: Optional[TenetsConfig] = None,
    **kwargs: Any
) -> BlameReport

Convenience function to analyze blame.

PARAMETERDESCRIPTION
repo_path

Path to repository

TYPE:Path

target

File or directory to analyze

TYPE:strDEFAULT:'.'

config

Optional configuration

TYPE:Optional[TenetsConfig]DEFAULT:None

**kwargs

Additional arguments

TYPE:AnyDEFAULT:{}

RETURNSDESCRIPTION
BlameReport

Blame analysis report

TYPE:BlameReport

Example

from tenets.core.git.blame import analyze_blame report = analyze_blame(Path("."), target="src/") print(f"Bus factor: {report.bus_factor}")

Source code in tenets/core/git/blame.py
Python
def analyze_blame(
    repo_path: Path, target: str = ".", config: Optional[TenetsConfig] = None, **kwargs: Any
) -> BlameReport:
    """Convenience function to analyze blame.

    Args:
        repo_path: Path to repository
        target: File or directory to analyze
        config: Optional configuration
        **kwargs: Additional arguments

    Returns:
        BlameReport: Blame analysis report

    Example:
        >>> from tenets.core.git.blame import analyze_blame
        >>> report = analyze_blame(Path("."), target="src/")
        >>> print(f"Bus factor: {report.bus_factor}")
    """
    if config is None:
        config = TenetsConfig()

    analyzer = BlameAnalyzer(config)

    target_path = Path(repo_path) / target

    if target_path.is_file():
        # Single file analysis
        file_blame = analyzer.analyze_file(repo_path, target)
        report = BlameReport(files_analyzed=1)
        report.file_blames[target] = file_blame
        analyzer._calculate_report_stats(report)
        report.recommendations = analyzer._generate_recommendations(report)
        return report
    else:
        # Directory analysis
        return analyzer.analyze_directory(repo_path, target, **kwargs)