• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 25350464842

05 May 2026 12:06AM UTC coverage: 92.923% (-0.002%) from 92.925%
25350464842

push

github

web-flow
Differentiate types of git changes (Cherry-pick of #23309) (#23313)

When interrogating git to compute `--changed-*`
specs, we now record the type of git change 
(add, modify, delete).

We don't yet do anything with this information.
This is initial work towards fixing #17512/#23240.

Co-authored-by: Benjy Weinberger <benjyw@gmail.com>

54 of 59 new or added lines in 4 files covered. (91.53%)

1 existing line in 1 file now uncovered.

91716 of 98701 relevant lines covered (92.92%)

4.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.61
/src/python/pants/vcs/changed.py
1
# Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
12✔
5

6
from collections.abc import Iterable
12✔
7
from dataclasses import dataclass
12✔
8
from enum import Enum
12✔
9

10
from pants.backend.project_info import dependents
12✔
11
from pants.backend.project_info.dependents import DependentsRequest, find_dependents
12✔
12
from pants.base.build_environment import get_buildroot
12✔
13
from pants.engine.addresses import Address, Addresses
12✔
14
from pants.engine.collection import Collection
12✔
15
from pants.engine.internals.graph import OwnersRequest, find_owners, resolve_unexpanded_targets
12✔
16
from pants.engine.internals.mapper import SpecsFilter
12✔
17
from pants.engine.rules import collect_rules, implicitly, rule
12✔
18
from pants.option.option_types import EnumOption, StrOption
12✔
19
from pants.option.option_value_container import OptionValueContainer
12✔
20
from pants.option.subsystem import Subsystem
12✔
21
from pants.util.docutil import doc_url
12✔
22
from pants.util.frozendict import FrozenDict
12✔
23
from pants.util.ordered_set import FrozenOrderedSet
12✔
24
from pants.util.strutil import help_text
12✔
25
from pants.vcs.git import GitWorktree
12✔
26
from pants.vcs.hunk import Hunk, TextBlocks
12✔
27

28

29
class DependentsOption(Enum):
12✔
30
    NONE = "none"
12✔
31
    DIRECT = "direct"
12✔
32
    TRANSITIVE = "transitive"
12✔
33

34

35
@dataclass(frozen=True)
12✔
36
class ChangedRequest:
12✔
37
    sources: tuple[str, ...]
12✔
38
    sources_blocks: FrozenDict[str, TextBlocks]
12✔
39
    dependents: DependentsOption
12✔
40

41

42
class ChangedAddresses(Collection[Address]):
12✔
43
    pass
12✔
44

45

46
@rule
12✔
47
async def find_changed_owners(
12✔
48
    request: ChangedRequest,
49
    specs_filter: SpecsFilter,
50
) -> ChangedAddresses:
51
    no_dependents = request.dependents == DependentsOption.NONE
×
52
    owners = await find_owners(
×
53
        OwnersRequest(
54
            request.sources,
55
            # If `--changed-dependents` is used, we cannot eagerly filter out root targets. We
56
            # need to first find their dependents, and only then should we filter. See
57
            # https://github.com/pantsbuild/pants/issues/15544
58
            filter_by_global_options=no_dependents,
59
            # Changing a BUILD file might impact the targets it defines.
60
            match_if_owning_build_file_included_in_sources=True,
61
            sources_blocks=request.sources_blocks,
62
        ),
63
        **implicitly(),
64
    )
65

66
    if no_dependents:
×
67
        return ChangedAddresses(owners)
×
68

69
    # See https://github.com/pantsbuild/pants/issues/15313. We filter out target generators because
70
    # they are not useful as aliases for their generated targets in the context of
71
    # `--changed-since`. Including them makes it look like all sibling targets from the same
72
    # target generator have also changed.
73
    #
74
    # However, we also must be careful to preserve if target generators are direct owners, which
75
    # happens when a generated file is deleted.
76
    owner_target_generators = FrozenOrderedSet(
×
77
        addr.maybe_convert_to_target_generator() for addr in owners if addr.is_generated_target
78
    )
79
    dependents = await find_dependents(
×
80
        DependentsRequest(
81
            owners,
82
            transitive=request.dependents == DependentsOption.TRANSITIVE,
83
            include_roots=False,
84
        ),
85
        **implicitly(),
86
    )
87
    result = FrozenOrderedSet(owners) | (dependents - owner_target_generators)
×
88
    if specs_filter.is_specified:
×
89
        # Finally, we must now filter out the result to only include what matches our tags, as the
90
        # last step of https://github.com/pantsbuild/pants/issues/15544.
91
        #
92
        # Note that we use `UnexpandedTargets` rather than `Targets` or `FilteredTargets` so that
93
        # we preserve target generators.
94
        result_as_tgts = await resolve_unexpanded_targets(Addresses(result))
×
95
        result = FrozenOrderedSet(
×
96
            tgt.address for tgt in result_as_tgts if specs_filter.matches(tgt)
97
        )
98

99
    return ChangedAddresses(result)
×
100

101

102
@dataclass(frozen=True)
12✔
103
class ChangedOptions:
12✔
104
    """A wrapper for the options from the `Changed` Subsystem.
105

106
    This is necessary because parsing of these options happens before conventional subsystems are
107
    configured, so the normal mechanisms like `Subsystem.rules()` would not work properly.
108
    """
109

110
    since: str | None
12✔
111
    diffspec: str | None
12✔
112
    dependents: DependentsOption
12✔
113

114
    @classmethod
12✔
115
    def from_options(cls, options: OptionValueContainer) -> ChangedOptions:
12✔
116
        return cls(options.since, options.diffspec, options.dependents)
×
117

118
    @property
12✔
119
    def provided(self) -> bool:
12✔
120
        return bool(self.since) or bool(self.diffspec)
×
121

122
    def changed_files(self, git_worktree: GitWorktree) -> set[str]:
12✔
123
        """Determines the files changed according to SCM/workspace and options."""
124
        if self.diffspec:
×
NEW
125
            return {
×
126
                cf.path
127
                for cf in git_worktree.changes_in(self.diffspec, relative_to=get_buildroot())
128
            }
129

130
        changes_since = self.since or git_worktree.current_rev_identifier
×
NEW
131
        return {
×
132
            cf.path
133
            for cf in git_worktree.changed_files(
134
                from_commit=changes_since,
135
                include_untracked=True,
136
                relative_to=get_buildroot(),
137
            )
138
        }
139

140
    def diff_hunks(
12✔
141
        self, git_worktree: GitWorktree, paths: Iterable[str]
142
    ) -> dict[str, tuple[Hunk, ...]]:
143
        """Determines the unified diff hunks changed according to SCM/workspace and options.
144

145
        More info on unified diff: https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html
146
        """
147
        changes_since = self.since or git_worktree.current_rev_identifier
×
148
        return git_worktree.changed_files_lines(
×
149
            paths,
150
            from_commit=changes_since,
151
            include_untracked=True,
152
            relative_to=get_buildroot(),
153
        )
154

155

156
class Changed(Subsystem):
12✔
157
    options_scope = "changed"
12✔
158
    help = help_text(
12✔
159
        f"""
160
        Tell Pants to detect what files and targets have changed from Git.
161

162
        See {doc_url("docs/using-pants/advanced-target-selection")}.
163
        """
164
    )
165

166
    since = StrOption(
12✔
167
        default=None,
168
        help="Calculate changes since this Git spec (commit range/SHA/ref).",
169
    )
170
    diffspec = StrOption(
12✔
171
        default=None,
172
        help="Calculate changes contained within a given Git spec (commit range/SHA/ref).",
173
    )
174
    dependents = EnumOption(
12✔
175
        default=DependentsOption.NONE,
176
        help="Include direct or transitive dependents of changed targets.",
177
    )
178

179

180
def rules():
12✔
181
    return [*collect_rules(), *dependents.rules()]
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc