File: git_diff.py

package info (click to toggle)
diff-cover 10.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,252 kB
  • sloc: python: 6,425; xml: 218; cpp: 18; sh: 12; makefile: 10
file content (151 lines) | stat: -rw-r--r-- 4,930 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""
Wrapper for `git diff` command.
"""

from textwrap import dedent

from diff_cover.command_runner import CommandError, execute
from diff_cover.util import to_unescaped_filename


class GitDiffError(Exception):
    """
    `git diff` command produced an error.
    """


class GitDiffTool:
    """
    Thin wrapper for a subset of the `git diff` command.
    """

    def __init__(self, range_notation, ignore_whitespace):
        """
        :param str range_notation:
            which range notation to use when producing the diff for committed
            files against another branch.

            Traditionally in git-cover the symmetric difference (three-dot, "A...M") notation has
            been used:
            it includes commits reachable from A and M from their merge-base, but not both,
            taking history in account.
            This includes cherry-picks between A and M, which are harmless and do not produce
            changes, but might give inaccurate coverage false-negatives.

            Two-dot range notation ("A..M") compares the tips of both trees and produces a diff.
            This more accurately describes the actual patch that will be applied by merging A into
            M, even if commits have been cherry-picked between branches.
            This will produce a more accurate diff for coverage comparison when complex merges and
            cherry-picks are involved.

         :param bool ignore_whitespace:
            Perform a diff but ignore any and all whitespace.
        """
        self._untracked_cache = None
        self.range_notation = range_notation
        self._default_git_args = [
            "git",
            "-c",
            "diff.mnemonicprefix=no",
            "-c",
            "diff.noprefix=no",
        ]

        self._default_diff_args = ["diff", "--no-color", "--no-ext-diff", "-U0"]

        if ignore_whitespace:
            self._default_diff_args.append("--ignore-all-space")
            self._default_diff_args.append("--ignore-blank-lines")

    def diff_committed(self, compare_branch="origin/main"):
        """
        Returns the output of `git diff` for committed
        changes not yet in origin/main.

        Raises a `GitDiffError` if `git diff` outputs anything
        to stderr.
        """
        diff_range = f"{compare_branch}{self.range_notation}HEAD"
        try:
            return execute(
                self._default_git_args + self._default_diff_args + [diff_range]
            )[0]
        except CommandError as e:
            if "unknown revision" in str(e):
                raise ValueError(
                    dedent(
                        f"""
                        Could not find the branch to compare to. Does '{compare_branch}' exist?
                        the `--compare-branch` argument allows you to set a different branch.
                    """
                    )
                ) from e
            raise

    def diff_unstaged(self):
        """
        Returns the output of `git diff` with no arguments, which
        is the diff for unstaged changes.

        Raises a `GitDiffError` if `git diff` outputs anything
        to stderr.
        """
        return execute(self._default_git_args + self._default_diff_args)[0]

    def diff_staged(self):
        """
        Returns the output of `git diff --cached`, which
        is the diff for staged changes.

        Raises a `GitDiffError` if `git diff` outputs anything
        to stderr.
        """
        return execute(self._default_git_args + self._default_diff_args + ["--cached"])[
            0
        ]

    def untracked(self):
        """Return the untracked files."""
        if self._untracked_cache is not None:
            return self._untracked_cache

        output = execute(["git", "ls-files", "--exclude-standard", "--others"])[0]
        self._untracked_cache = []
        if output:
            self._untracked_cache = [
                to_unescaped_filename(line) for line in output.splitlines() if line
            ]
        return self._untracked_cache


class GitDiffFileTool(GitDiffTool):

    def __init__(self, diff_file_path):

        self.diff_file_path = diff_file_path
        super().__init__("...", False)

    def diff_committed(self, compare_branch="origin/main"):
        """
        Returns the contents of a diff file.

        Raises a `GitDiffError` if the file cannot be read.
        """
        try:
            with open(self.diff_file_path, "r", encoding="utf-8") as file:
                return file.read()
        except OSError as e:
            error_message = (
                "Could not read the diff file. "
                f"Make sure '{self.diff_file_path}' exists?"
            )
            raise ValueError(error_message) from e

    def diff_unstaged(self):
        return ""

    def diff_staged(self):
        return ""

    def untracked(self):
        return ""