Skip to content

Git Provider

codebrief.tools.git_provider

Git Context Provider for CodeBrief.

This module provides functions to extract Git context information from a project repository, including current branch, status, uncommitted changes, and recent commits. It uses subprocess calls to interact with Git and formats the output as structured Markdown suitable for inclusion in context bundles.

Core functionalities: - Extract current branch name - Get Git status summary - List uncommitted changes with file status - Retrieve recent commit history - Support for optional diff output (controlled by parameters) - Graceful handling of non-Git repositories - Structured Markdown output with clear sectioning

Functions

get_git_context(project_root, diff_options=None, log_count=5, full_diff=False)

Extract Git context information from a project repository.

This function executes various Git commands to gather comprehensive context about the repository state, including branch information, file changes, and commit history. All output is formatted as structured Markdown.

Parameters:

NameTypeDescriptionDefault
project_rootPath

The root directory of the Git repository

required
diff_optionsOptional[str]

Optional string for advanced git diff options (e.g., "--stat", "src/myfile.py")

None
log_countint

Number of recent commits to include (default: 5)

5
full_diffbool

If True, include full diff output (default: False)

False

Returns:

TypeDescription
str

Formatted Markdown string containing Git context information

Note

No exceptions are raised; all errors are captured and included in the output

Source code in src/codebrief/tools/git_provider.py
def get_git_context(
    project_root: Path,
    diff_options: Optional[str] = None,
    log_count: int = 5,
    full_diff: bool = False,
) -> str:
    """
    Extract Git context information from a project repository.

    This function executes various Git commands to gather comprehensive context
    about the repository state, including branch information, file changes,
    and commit history. All output is formatted as structured Markdown.

    Args:
        project_root: The root directory of the Git repository
        diff_options: Optional string for advanced git diff options (e.g., "--stat", "src/myfile.py")
        log_count: Number of recent commits to include (default: 5)
        full_diff: If True, include full diff output (default: False)

    Returns:
        Formatted Markdown string containing Git context information

    Note:
        No exceptions are raised; all errors are captured and included in the output
    """
    if not project_root.exists():
        return (
            f"# Git Context\n\nError: Project path '{project_root}' does not exist.\n"
        )

    if not project_root.is_dir():
        return f"# Git Context\n\nError: Project path '{project_root}' is not a directory.\n"

    # Check if git executable is available
    try:
        subprocess.run(  # nosec B603, B607
            ["git", "--version"],
            capture_output=True,
            check=True,
            timeout=10,
        )
    except FileNotFoundError:
        return "# Git Context\n\nError: Git executable not found. Please ensure Git is installed and available in PATH.\n"
    except subprocess.TimeoutExpired:
        return "# Git Context\n\nError: Git command timed out.\n"
    except subprocess.CalledProcessError:
        return "# Git Context\n\nError: Git executable found but returned an error.\n"

    # Check if this is a Git repository
    try:
        result = subprocess.run(  # nosec B603, B607
            ["git", "rev-parse", "--is-inside-work-tree"],
            cwd=project_root,
            capture_output=True,
            check=True,
            text=True,
            timeout=10,
        )
        if result.stdout.strip() != "true":
            return "# Git Context\n\nNot a Git repository or no Git history.\n"
    except subprocess.CalledProcessError:
        return "# Git Context\n\nNot a Git repository or no Git history.\n"
    except subprocess.TimeoutExpired:
        return "# Git Context\n\nError: Git command timed out while checking repository status.\n"
    except Exception as e:
        return f"# Git Context\n\nError checking Git repository: {e}\n"

    # Now we know it's a valid Git repository, gather information
    markdown_sections = ["# Git Context\n"]

    # 1. Get current branch
    try:
        result = subprocess.run(  # nosec B603, B607
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            cwd=project_root,
            capture_output=True,
            check=True,
            text=True,
            timeout=10,
        )
        current_branch = result.stdout.strip()
        markdown_sections.append("## Current Branch\n")
        markdown_sections.append("```")
        markdown_sections.append(current_branch)
        markdown_sections.append("```\n")
    except subprocess.CalledProcessError as e:
        markdown_sections.append("## Current Branch\n")
        markdown_sections.append("```")
        markdown_sections.append(
            f"Error getting current branch: {e.stderr.strip() if e.stderr else 'Unknown error'}"
        )
        markdown_sections.append("```\n")
    except subprocess.TimeoutExpired:
        markdown_sections.append("## Current Branch\n")
        markdown_sections.append("```")
        markdown_sections.append("Error: Git command timed out")
        markdown_sections.append("```\n")
    except Exception as e:
        markdown_sections.append("## Current Branch\n")
        markdown_sections.append("```")
        markdown_sections.append(f"Error: {e}")
        markdown_sections.append("```\n")

    # 2. Get Git status
    try:
        result = subprocess.run(  # nosec B603, B607
            ["git", "status", "--short"],
            cwd=project_root,
            capture_output=True,
            check=True,
            text=True,
            timeout=10,
        )
        git_status = result.stdout.strip()
        markdown_sections.append("## Git Status\n")
        markdown_sections.append("```")
        if git_status:
            markdown_sections.append(git_status)
        else:
            markdown_sections.append("Working tree clean")
        markdown_sections.append("```\n")
    except subprocess.CalledProcessError as e:
        markdown_sections.append("## Git Status\n")
        markdown_sections.append("```")
        markdown_sections.append(
            f"Error getting Git status: {e.stderr.strip() if e.stderr else 'Unknown error'}"
        )
        markdown_sections.append("```\n")
    except subprocess.TimeoutExpired:
        markdown_sections.append("## Git Status\n")
        markdown_sections.append("```")
        markdown_sections.append("Error: Git command timed out")
        markdown_sections.append("```\n")
    except Exception as e:
        markdown_sections.append("## Git Status\n")
        markdown_sections.append("```")
        markdown_sections.append(f"Error: {e}")
        markdown_sections.append("```\n")

    # 3. Get uncommitted changes (tracked files)
    try:
        result = subprocess.run(  # nosec B603, B607
            ["git", "diff", "HEAD", "--name-status"],
            cwd=project_root,
            capture_output=True,
            check=True,
            text=True,
            timeout=15,
        )
        uncommitted_changes = result.stdout.strip()
        markdown_sections.append("## Uncommitted Changes (Tracked Files)\n")
        markdown_sections.append("```")
        if uncommitted_changes:
            markdown_sections.append(uncommitted_changes)
        else:
            markdown_sections.append("No uncommitted changes to tracked files")
        markdown_sections.append("```\n")
    except subprocess.CalledProcessError as e:
        markdown_sections.append("## Uncommitted Changes (Tracked Files)\n")
        markdown_sections.append("```")
        markdown_sections.append(
            f"Error getting uncommitted changes: {e.stderr.strip() if e.stderr else 'Unknown error'}"
        )
        markdown_sections.append("```\n")
    except subprocess.TimeoutExpired:
        markdown_sections.append("## Uncommitted Changes (Tracked Files)\n")
        markdown_sections.append("```")
        markdown_sections.append("Error: Git command timed out")
        markdown_sections.append("```\n")
    except Exception as e:
        markdown_sections.append("## Uncommitted Changes (Tracked Files)\n")
        markdown_sections.append("```")
        markdown_sections.append(f"Error: {e}")
        markdown_sections.append("```\n")

    # 4. Get recent commits
    try:
        result = subprocess.run(  # nosec B603, B607
            ["git", "log", "-n", str(log_count), "--oneline", "--decorate", "--graph"],
            cwd=project_root,
            capture_output=True,
            check=True,
            text=True,
            timeout=15,
        )
        recent_commits = result.stdout.strip()
        markdown_sections.append(f"## Recent Commits (Last {log_count})\n")
        markdown_sections.append("```")
        if recent_commits:
            markdown_sections.append(recent_commits)
        else:
            markdown_sections.append("No commits found")
        markdown_sections.append("```\n")
    except subprocess.CalledProcessError as e:
        markdown_sections.append(f"## Recent Commits (Last {log_count})\n")
        markdown_sections.append("```")
        markdown_sections.append(
            f"Error getting recent commits: {e.stderr.strip() if e.stderr else 'Unknown error'}"
        )
        markdown_sections.append("```\n")
    except subprocess.TimeoutExpired:
        markdown_sections.append(f"## Recent Commits (Last {log_count})\n")
        markdown_sections.append("```")
        markdown_sections.append("Error: Git command timed out")
        markdown_sections.append("```\n")
    except Exception as e:
        markdown_sections.append(f"## Recent Commits (Last {log_count})\n")
        markdown_sections.append("```")
        markdown_sections.append(f"Error: {e}")
        markdown_sections.append("```\n")

    # 5. Optional full diff or custom diff options
    if full_diff or diff_options:
        try:
            diff_cmd = ["git", "diff", "HEAD"]
            if diff_options:
                # Split diff_options and add to command
                # This is a simple split - in production, you might want more sophisticated parsing
                diff_cmd.extend(diff_options.split())

            result = subprocess.run(  # nosec B603, B607
                diff_cmd,
                cwd=project_root,
                capture_output=True,
                check=True,
                text=True,
                timeout=30,  # Longer timeout for diff operations
            )
            diff_output = result.stdout.strip()

            diff_title = "## Full Diff" if full_diff else f"## Diff ({diff_options})"
            markdown_sections.append(f"{diff_title}\n")
            markdown_sections.append("```diff")
            if diff_output:
                markdown_sections.append(diff_output)
            else:
                markdown_sections.append("No differences found")
            markdown_sections.append("```\n")
        except subprocess.CalledProcessError as e:
            diff_title = "## Full Diff" if full_diff else f"## Diff ({diff_options})"
            markdown_sections.append(f"{diff_title}\n")
            markdown_sections.append("```")
            markdown_sections.append(
                f"Error getting diff: {e.stderr.strip() if e.stderr else 'Unknown error'}"
            )
            markdown_sections.append("```\n")
        except subprocess.TimeoutExpired:
            diff_title = "## Full Diff" if full_diff else f"## Diff ({diff_options})"
            markdown_sections.append(f"{diff_title}\n")
            markdown_sections.append("```")
            markdown_sections.append("Error: Git diff command timed out")
            markdown_sections.append("```\n")
        except Exception as e:
            diff_title = "## Full Diff" if full_diff else f"## Diff ({diff_options})"
            markdown_sections.append(f"{diff_title}\n")
            markdown_sections.append("```")
            markdown_sections.append(f"Error: {e}")
            markdown_sections.append("```\n")

    return "\n".join(markdown_sections)

Overview

The git_provider module provides comprehensive Git repository context extraction functionality. It safely executes Git commands to gather repository information including branch status, commit history, and change diffs.

Key Features

  • Safe Git Command Execution: Robust subprocess handling with timeout protection
  • Comprehensive Context: Branch info, status, commits, and diffs
  • Error Resilience: Graceful handling of non-Git repos and command failures
  • Security Compliant: Bandit-approved subprocess usage for Git operations
  • Configurable Output: Flexible diff options and commit count limits

Functions

get_git_context

Extract complete Git repository context information.

Parameters: - project_root (Path): Root directory of the project/repository - log_count (int, optional): Number of recent commits to include (default: 10) - full_diff (bool, optional): Include full diff of uncommitted changes (default: False) - diff_options (str, optional): Custom git diff options (default: None)

Returns: - str: Formatted Markdown string containing Git context

Raises: - No exceptions raised - all errors are handled gracefully and returned as informative messages

Usage Examples

Basic Usage

from pathlib import Path
from codebrief.tools.git_provider import get_git_context

# Get basic Git context
context = get_git_context(Path("."))
print(context)

Advanced Usage

# Get detailed context with full diff
context = get_git_context(
    project_root=Path("/path/to/repo"),
    log_count=5,
    full_diff=True,
    diff_options="--stat --color=never"
)

CLI Integration

# This is how the CLI command uses the function
from codebrief.tools.git_provider import get_git_context

def git_info_command(
    root_dir: Path,
    output: Optional[Path] = None,
    log_count: int = 10,
    full_diff: bool = False,
    diff_options: Optional[str] = None,
):
    context = get_git_context(
        project_root=root_dir,
        log_count=log_count,
        full_diff=full_diff,
        diff_options=diff_options,
    )

    if output:
        output.write_text(context, encoding="utf-8")
    else:
        console.print(context)

Output Format

The function returns a structured Markdown document:

# Git Context

## Repository Information
- **Current Branch:** main
- **Repository Status:** Clean working directory

## Recent Commits (Last 10)
1. **feat: add new feature** (2024-01-15 14:30:25)
   - Author: Developer <dev@example.com>
   - Hash: abc123f

2. **fix: resolve parsing issue** (2024-01-14 09:15:42)
   - Author: Developer <dev@example.com>
   - Hash: def456a

## Uncommitted Changes
*No uncommitted changes*

## Full Diff
[Included when full_diff=True]

## Diff (--stat)
[Included when diff_options provided]

Error Handling

The module handles various error scenarios gracefully:

Non-Git Repository

context = get_git_context(Path("/tmp"))
# Returns: "# Git Context\n\nNot a Git repository or no Git history.\n"

Git Not Available

# When Git is not installed
# Returns: "# Git Context\n\nError: Git executable not found..."

Permission Issues

# When Git commands fail due to permissions
# Returns appropriate error message with context

Security Considerations

  • All subprocess calls use explicit command arrays (not shell strings)
  • Timeout protection prevents hanging on slow Git operations
  • Input validation ensures safe path handling
  • Bandit security compliance with appropriate # nosec annotations

Testing

The module includes comprehensive test coverage:

  • 13 test cases covering all functionality
  • Error scenario testing for robustness
  • Integration testing with real Git repositories
  • Parameter validation testing
  • Security compliance verification

Configuration Integration

Works seamlessly with codebrief's configuration system:

[tool.codebrief]
default_output_filename_git_info = "git-context.md"

Dependencies

  • subprocess: For Git command execution
  • pathlib: For path handling
  • typing: For type annotations
  • bundler: Uses git_provider for bundle Git sections
  • config_manager: Provides configuration defaults
  • main: CLI integration and command handling