<commit_before>"""
Classes for querying which lines have changed based on a diff.
"""

from abc import ABCMeta, abstractmethod
import subprocess
import re

class BaseDiffReporter(object):
    """
    Query information about lines changed in a diff.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def src_paths_changed(self):
        """
        Returns a list of source paths changed in this diff.
        """
        pass

    @abstractmethod
    def hunks_changed(self, src_path):
        """
        Returns a list of hunks changed in the source file at `src_path`.
        Each hunk is a `(start_line, end_line)` tuple indicating
        the starting and ending line numbers of the hunk
        in the current version of the source file.
        """
        pass

class GitDiffError(Exception):
    """
    `git diff` command exited with non-zero status,
    or `git diff` produced invalid output.
    """
    pass

class GitDiffReporter(BaseDiffReporter):
    """
    Query information from a Git diff between branches.
    """

    def __init__(self, compare_branch, subprocess_mod=subprocess):
        """
        Configure the reporter to compare the current repo branch
        with `compare_branch`.

        Uses `subprocess_mod` to perform the system call to
        `git diff`.  
        """
        super(GitDiffReporter, self).__init__()

        self._compare_branch = compare_branch
        self._subprocess_mod = subprocess_mod

        # Cache diff information as a dictionary
        # with file path keys and hunk list values
        self._diff_dict = None

    def src_paths_changed(self):

        # Get the diff dictionary
        diff_dict = self._git_diff()

        # Return the changed file paths (dict keys)
        return diff_dict.keys()

    def hunks_changed(self, src_path):

        # Get the diff dictionary
        diff_dict = self._git_diff()

        # Return the list `(start_line, end_line)` hunks
        # for the file at `src_path`
        # If no such file, return an empty list.
        return diff_dict.get(src_path, [])

    def _git_diff(self):
        """
        Run `git diff` and returns a dict in which the keys
        are changed file paths and the values are lists of 
        `(start_line, end_line)` tuples.

        Returns a cached result if called multiple times.

        Raises a GitDiffError if `git diff` has an error exit status.
        """
        
        # If we do not have a cached result, execute `git diff`
        if self._diff_dict is None:

            # Get the output of `git diff`
            diff_str = self._git_diff_str()

            # Parse the output of the diff string
            self._diff_dict = self._parse_diff_str(diff_str)

        # Return the diff cache
        return self._diff_dict

    def _git_diff_str(self):
        """
        Execute `git diff` and return the output string,
        raising a GitDiffError if non-zero exit status.
        """
        command = ['git', 'diff', self._compare_branch]
        stdout_pipe = self._subprocess_mod.PIPE

        # Execute `git diff` and capture output to stdout
        process = self._subprocess_mod.Popen(command, stdout=stdout_pipe,
                                                      stderr=stdout_pipe)
        output, err = process.communicate()

        # If an error with git diff, raise an exception
        if err is not '':
            raise GitDiffError(str(err))

        # Return the output string
        return output

    # Regular expressions used to parse the diff output
    SRC_FILE_RE = re.compile('^diff --git a/.* b/([^ \n]*)')
    HUNK_LINE_RE = re.compile('^@@ -.* \+([0-9,]*) @@')

    def _parse_diff_str(self, diff_str):
        """
        Parse the output of `git diff` into a dictionary with
        keys that are the source file paths, and values
        that are lists of `(start_line, end_line)` hunks
        changed.
<commit_msg>Fixed an issue with empty string comparison on git diff errors.<commit_after>"""
Classes for querying which lines have changed based on a diff.
"""

from abc import ABCMeta, abstractmethod
import subprocess
import re

class BaseDiffReporter(object):
    """
    Query information about lines changed in a diff.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def src_paths_changed(self):
        """
        Returns a list of source paths changed in this diff.
        """
        pass

    @abstractmethod
    def hunks_changed(self, src_path):
        """
        Returns a list of hunks changed in the source file at `src_path`.
        Each hunk is a `(start_line, end_line)` tuple indicating
        the starting and ending line numbers of the hunk
        in the current version of the source file.
        """
        pass

class GitDiffError(Exception):
    """
    `git diff` command exited with non-zero status,
    or `git diff` produced invalid output.
    """
    pass

class GitDiffReporter(BaseDiffReporter):
    """
    Query information from a Git diff between branches.
    """

    def __init__(self, compare_branch, subprocess_mod=subprocess):
        """
        Configure the reporter to compare the current repo branch
        with `compare_branch`.

        Uses `subprocess_mod` to perform the system call to
        `git diff`.  
        """
        super(GitDiffReporter, self).__init__()

        self._compare_branch = compare_branch
        self._subprocess_mod = subprocess_mod

        # Cache diff information as a dictionary
        # with file path keys and hunk list values
        self._diff_dict = None

    def src_paths_changed(self):

        # Get the diff dictionary
        diff_dict = self._git_diff()

        # Return the changed file paths (dict keys)
        return diff_dict.keys()

    def hunks_changed(self, src_path):

        # Get the diff dictionary
        diff_dict = self._git_diff()

        # Return the list `(start_line, end_line)` hunks
        # for the file at `src_path`
        # If no such file, return an empty list.
        return diff_dict.get(src_path, [])

    def _git_diff(self):
        """
        Run `git diff` and returns a dict in which the keys
        are changed file paths and the values are lists of 
        `(start_line, end_line)` tuples.

        Returns a cached result if called multiple times.

        Raises a GitDiffError if `git diff` has an error exit status.
        """
        
        # If we do not have a cached result, execute `git diff`
        if self._diff_dict is None:

            # Get the output of `git diff`
            diff_str = self._git_diff_str()

            # Parse the output of the diff string
            self._diff_dict = self._parse_diff_str(diff_str)

        # Return the diff cache
        return self._diff_dict

    def _git_diff_str(self):
        """
        Execute `git diff` and return the output string,
        raising a GitDiffError if non-zero exit status.
        """
        command = ['git', 'diff', self._compare_branch]
        stdout_pipe = self._subprocess_mod.PIPE

        # Execute `git diff` and capture output to stdout
        process = self._subprocess_mod.Popen(command, stdout=stdout_pipe,
                                                      stderr=stdout_pipe)
        output, err = process.communicate()

        # If an error with git diff, raise an exception
        if bool(err):
            raise GitDiffError(str(err))

        # Return the output string
        return output

    # Regular expressions used to parse the diff output
    SRC_FILE_RE = re.compile('^diff --git a/.* b/([^ \n]*)')
    HUNK_LINE_RE = re.compile('^@@ -.* \+([0-9,]*) @@')

    def _parse_diff_str(self, diff_str):
        """
        Parse the output of `git diff` into a dictionary with
        keys that are the source file paths, and values
        that are lists of `(start_line, end_line)` hunks
        changed.

{'fraction_non_alphanumeric': 0.07848837209302326, 'fraction_numerical': 0.0005055611729019212, 'mean_word_length': 2.156362185879537, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6245133', 'n_tokens_mistral': 2208, 'n_tokens_neox': 2133, 'n_words': 804}