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¶
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.
| ATTRIBUTE | DESCRIPTION |
|---|---|
line_number | Line number in file TYPE: |
content | Content of the line TYPE: |
author | Author name TYPE: |
author_email | Author email TYPE: |
commit_sha | Commit SHA that introduced this line TYPE: |
commit_date | Date when line was introduced TYPE: |
commit_message | Commit message (first line) TYPE: |
is_original | Whether this is from the original commit TYPE: |
age_days | Age of the line in days TYPE: |
previous_authors | List of previous authors if line was modified |
Attributes¶
is_recentproperty¶
Check if line was recently modified.
| RETURNS | DESCRIPTION |
|---|---|
bool | True if modified within last 30 days TYPE: |
FileBlamedataclass¶
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.
| ATTRIBUTE | DESCRIPTION |
|---|---|
file_path | Path to the file TYPE: |
total_lines | Total number of lines TYPE: |
blame_lines | List of blame information per line |
authors | Set of unique authors |
author_stats | Statistics per author |
commit_shas | Set of unique commits |
oldest_line | Oldest line in file |
newest_line | Newest line in file |
age_distribution | Distribution of line ages |
ownership_map | Line ranges owned by each author |
hot_spots | Lines that changed frequently |
Attributes¶
primary_authorproperty¶
author_diversityproperty¶
Calculate author diversity score.
Higher scores indicate more distributed authorship.
| RETURNS | DESCRIPTION |
|---|---|
float | Diversity score (0-1) TYPE: |
BlameReportdataclass¶
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.
| ATTRIBUTE | DESCRIPTION |
|---|---|
files_analyzed | Number of files analyzed TYPE: |
total_lines | Total lines analyzed TYPE: |
total_authors | Total unique authors TYPE: |
file_blames | Blame data for each file |
author_summary | Summary statistics per author |
ownership_distribution | How ownership is distributed |
collaboration_matrix | Who modified whose code |
knowledge_map | Knowledge areas per author |
recommendations | Actionable recommendations |
hot_files | Files with most contributors |
single_author_files | Files with only one author |
abandoned_code | Code from inactive authors |
Attributes¶
bus_factorpropertywritable¶
Calculate bus factor based on blame data.
| RETURNS | DESCRIPTION |
|---|---|
int | Bus factor (number of critical authors) TYPE: |
collaboration_scorepropertywritable¶
Calculate collaboration score.
Higher scores indicate more collaborative development.
| RETURNS | DESCRIPTION |
|---|---|
float | Collaboration score (0-100) TYPE: |
Methods:¶
to_dict¶
Convert report to dictionary.
| RETURNS | DESCRIPTION |
|---|---|
Dict[str, Any] | Dict[str, Any]: Dictionary representation |
Source code in tenets/core/git/blame.py
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¶
Analyzer for git blame operations.
Provides line-by-line authorship analysis using git blame, helping understand code ownership and evolution patterns.
| ATTRIBUTE | DESCRIPTION |
|---|---|
config | Configuration object |
logger | Logger instance |
_blame_cache | Cache for blame results |
Initialize blame analyzer.
| PARAMETER | DESCRIPTION |
|---|---|
config | TenetsConfig instance TYPE: |
Source code in tenets/core/git/blame.py
Methods:¶
analyze_file¶
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.
| PARAMETER | DESCRIPTION |
|---|---|
repo_path | Path to git repository TYPE: |
file_path | Path to file relative to repo root TYPE: |
ignore_whitespace | Ignore whitespace changes TYPE: |
follow_renames | Follow file renames TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
FileBlame | Blame analysis for the file TYPE: |
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
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¶
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.
| PARAMETER | DESCRIPTION |
|---|---|
repo_path | Path to git repository TYPE: |
directory | Directory to analyze TYPE: |
file_pattern | File pattern to match TYPE: |
recursive | Whether to recurse into subdirectories TYPE: |
max_files | Maximum files to analyze TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
BlameReport | Comprehensive blame analysis TYPE: |
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
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¶
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.
| PARAMETER | DESCRIPTION |
|---|---|
repo_path | Path to git repository TYPE: |
file_path | Path to file TYPE: |
line_number | Line number to trace TYPE: |
max_depth | Maximum history depth to retrieve TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
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
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¶
analyze_blame(
repo_path: Path,
target: str = ".",
config: Optional[TenetsConfig] = None,
**kwargs: Any
) -> BlameReport
Convenience function to analyze blame.
| PARAMETER | DESCRIPTION |
|---|---|
repo_path | Path to repository TYPE: |
target | File or directory to analyze TYPE: |
config | Optional configuration TYPE: |
**kwargs | Additional arguments TYPE: |
| RETURNS | DESCRIPTION |
|---|---|
BlameReport | Blame analysis report TYPE: |
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
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)